diff --git a/README.md b/README.md index 0562113e5..8b3d70c27 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Name | Description [cisco.iosxr.iosxr_static_routes](https://github.com/ansible-collections/cisco.iosxr/blob/main/docs/cisco.iosxr.iosxr_static_routes_module.rst)|Resource module to configure static routes. [cisco.iosxr.iosxr_system](https://github.com/ansible-collections/cisco.iosxr/blob/main/docs/cisco.iosxr.iosxr_system_module.rst)|Module to manage the system attributes. [cisco.iosxr.iosxr_user](https://github.com/ansible-collections/cisco.iosxr/blob/main/docs/cisco.iosxr.iosxr_user_module.rst)|Module to manage the aggregates of local users. +[cisco.iosxr.iosxr_vrf_address_family](https://github.com/ansible-collections/cisco.iosxr/blob/main/docs/cisco.iosxr.iosxr_vrf_address_family_module.rst)|Resource module to configure VRF Address family. [cisco.iosxr.iosxr_vrf_global](https://github.com/ansible-collections/cisco.iosxr/blob/main/docs/cisco.iosxr.iosxr_vrf_global_module.rst)|Manages global VRF configuration. diff --git a/changelogs/fragments/add_vrfs_module.yaml b/changelogs/fragments/add_vrfs_module.yaml new file mode 100644 index 000000000..d23ab8c07 --- /dev/null +++ b/changelogs/fragments/add_vrfs_module.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Adds a new module `iosxr_vrf_address_family` to manage VRFs address families on Cisco IOS-XR devices (https://github.com/ansible-collections/cisco.iosxr/pull/489). diff --git a/meta/runtime.yml b/meta/runtime.yml index 21ee0a2c5..e6b8f953b 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -62,3 +62,5 @@ plugin_routing: redirect: cisco.iosxr.iosxr_hostname vrf_global: redirect: cisco.iosxr.iosxr_vrf_global + vrf_address_family: + redirect: cisco.iosxr.iosxr_vrf_address_family diff --git a/plugins/action/vrf_address_family.py b/plugins/action/vrf_address_family.py new file mode 120000 index 000000000..4f317eb5d --- /dev/null +++ b/plugins/action/vrf_address_family.py @@ -0,0 +1 @@ +iosxr.py \ No newline at end of file diff --git a/plugins/module_utils/network/iosxr/argspec/vrf_address_family/__init__.py b/plugins/module_utils/network/iosxr/argspec/vrf_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/iosxr/argspec/vrf_address_family/vrf_address_family.py b/plugins/module_utils/network/iosxr/argspec/vrf_address_family/vrf_address_family.py new file mode 100644 index 000000000..a49797ea2 --- /dev/null +++ b/plugins/module_utils/network/iosxr/argspec/vrf_address_family/vrf_address_family.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 iosxr_vrf_address_family module +""" + + +class Vrf_address_familyArgs(object): # pylint: disable=R0903 + """The arg spec for the iosxr_vrf_address_family module""" + + argument_spec = { + "config": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address_families": { + "type": "list", + "elements": "dict", + "options": { + "afi": {"type": "str", "choices": ["ipv4", "ipv6"]}, + "safi": { + "type": "str", + "choices": ["flowspec", "multicast", "unicast"], + }, + "export": { + "type": "dict", + "options": { + "route_policy": {"type": "str"}, + "route_target": {"type": "str"}, + "to": { + "type": "dict", + "options": { + "default_vrf": { + "type": "dict", + "options": { + "route_policy": {"type": "str"}, + }, + }, + "vrf": { + "type": "dict", + "options": { + "allow_imported_vpn": { + "type": "bool", + }, + }, + }, + }, + }, + }, + }, + "import_config": { + "type": "dict", + "options": { + "route_policy": {"type": "str"}, + "route_target": {"type": "str"}, + "from_config": { + "type": "dict", + "options": { + "bridge_domain": { + "type": "dict", + "options": { + "advertise_as_vpn": { + "type": "bool", + }, + }, + }, + "default_vrf": { + "type": "dict", + "options": { + "route_policy": {"type": "str"}, + }, + }, + "vrf": { + "type": "dict", + "options": { + "advertise_as_vpn": { + "type": "bool", + }, + }, + }, + }, + }, + }, + }, + "maximum": { + "type": "dict", + "options": {"prefix": {"type": "int"}}, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "parsed", + "gathered", + "deleted", + "merged", + "replaced", + "rendered", + "overridden", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/iosxr/config/vrf_address_family/__init__.py b/plugins/module_utils/network/iosxr/config/vrf_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/iosxr/config/vrf_address_family/vrf_address_family.py b/plugins/module_utils/network/iosxr/config/vrf_address_family/vrf_address_family.py new file mode 100644 index 000000000..1f40bce38 --- /dev/null +++ b/plugins/module_utils/network/iosxr/config/vrf_address_family/vrf_address_family.py @@ -0,0 +1,169 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 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 iosxr_vrf_address_family 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, +) + +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.facts import Facts +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.rm_templates.vrf_address_family import ( + Vrf_address_familyTemplate, +) + + +class Vrf_address_family(ResourceModule): + """ + The iosxr_vrf_address_family config class + """ + + def __init__(self, module): + super(Vrf_address_family, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vrf_address_family", + tmplt=Vrf_address_familyTemplate(), + ) + self.parsers = [ + "address_family", + "export.route_policy", + "export.route_target", + "export.to.default_vrf.route_policy", + "export.to.vrf.allow_imported_vpn", + "import_config.route_target", + "import_config.route_policy", + "import_config.from_config.bridge_domain.advertise_as_vpn", + "import_config.from_config.default_vrf.route_policy", + "import_config.from_config.vrf.advertise_as_vpn", + "maximum.prefix", + ] + + 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 = self.want + haved = self.have + + wantd = self._vrf_list_to_dict(wantd) + haved = self._vrf_list_to_dict(haved) + + # if state is merged, merge want into 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": + for vrfk, vrfv in iteritems(haved): + for afk, afv in iteritems(vrfv.get("address_families", {})): + adrf = wantd.get(vrfk, {}).get("address_families", {}) + if afk in adrf or not adrf: + self.addcmd( + {"name": vrfk}, + "name", + False, + ) + self.addcmd( + {"afi": afv.get("afi"), "safi": afv.get("safi")}, + "address_family", + True, + ) + + if self.state in ["overridden"]: + for vrfk, vrfv in iteritems(haved): + for k, have in iteritems(vrfv.get("address_families", {})): + wantx = wantd.get(vrfk, {}).get("address_families", {}) + if k not in wantx: + self.addcmd( + {"name": vrfk}, + "name", + False, + ) + self.addcmd( + {"afi": have.get("afi"), "safi": have.get("safi")}, + "address_family", + False, + ) + self.compare(parsers=self.parsers, want={}, have=have) + + if self.state != "deleted": + self._compare(want=wantd, have=haved) + + 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 Vrf network resource. + """ + for name, entry in iteritems(want): + begin = len(self.commands) + vrf_want = entry + vrf_have = have.pop(name, {}) + self._compare_afs(vrf_want, vrf_have) + if len(self.commands) != begin: + self.commands.insert(begin, "vrf {0}".format(name)) + + def _compare_afs(self, want, have): + """Custom handling of afs option + :params want: the want VRF dictionary + :params have: the have VRF dictionary + """ + waafs = want.get("address_families", {}) + haafs = have.get("address_families", {}) + for afk, afv in iteritems(waafs): + begin = len(self.commands) + self._compare_single_af(want=afv, have=haafs.get(afk, {})) + if len(self.commands) != begin: + self.commands.insert(begin, f"address-family {afv.get('afi')} {afv.get('safi')}") + + def _compare_single_af(self, want, have): + """Custom handling of single af option + :params want: the want VRF dictionary + :params have: the have VRF dictionary + """ + self.compare(parsers=self.parsers[1:], want=want, have=have) + + def _vrf_list_to_dict(self, entry): + """Convert list of items to dict of items + for efficient diff calculation. + :params entry: data dictionary + """ + + for vrf in entry: + if "address_families" in vrf: + vrf["address_families"] = { + f"{x['afi']}_{x.get('safi')}": x for x in vrf["address_families"] + } + entry = {x["name"]: x for x in entry} + return entry diff --git a/plugins/module_utils/network/iosxr/facts/facts.py b/plugins/module_utils/network/iosxr/facts/facts.py index 47c13c0a1..9588520ff 100644 --- a/plugins/module_utils/network/iosxr/facts/facts.py +++ b/plugins/module_utils/network/iosxr/facts/facts.py @@ -94,6 +94,9 @@ from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.static_routes.static_routes import ( Static_routesFacts, ) +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.vrf_address_family.vrf_address_family import ( + Vrf_address_familyFacts, +) from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.vrf_global.vrf_global import ( Vrf_globalFacts, ) @@ -129,6 +132,7 @@ snmp_server=Snmp_serverFacts, hostname=HostnameFacts, bgp_templates=Bgp_templatesFacts, + vrf_address_family=Vrf_address_familyFacts, vrf_global=Vrf_globalFacts, ) diff --git a/plugins/module_utils/network/iosxr/facts/vrf_address_family/__init__.py b/plugins/module_utils/network/iosxr/facts/vrf_address_family/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/iosxr/facts/vrf_address_family/vrf_address_family.py b/plugins/module_utils/network/iosxr/facts/vrf_address_family/vrf_address_family.py new file mode 100644 index 000000000..03d2c4c60 --- /dev/null +++ b/plugins/module_utils/network/iosxr/facts/vrf_address_family/vrf_address_family.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 iosxr vrf_address_family 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.iosxr.plugins.module_utils.network.iosxr.argspec.vrf_address_family.vrf_address_family import ( + Vrf_address_familyArgs, +) +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.rm_templates.vrf_address_family import ( + Vrf_address_familyTemplate, +) +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.utils.utils import ( + flatten_config, +) + + +class Vrf_address_familyFacts(object): + """The iosxr vrf_address_family facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vrf_address_familyArgs.argument_spec + + def get_config(self, connection): + """Get the configuration from the device""" + + return connection.get("show running-config vrf") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vrf_address_family network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + + facts = {} + objs = [] + obj = {} + + if not data: + data = self.get_config(connection) + + export_data = flatten_config(data, "export") + import_data = flatten_config(export_data, "import") + address_data = flatten_config(import_data, "address-family") + data = flatten_config(address_data, "vrf") + + # parse native config using the Vrf_address_family template + vrf_address_family_parser = Vrf_address_familyTemplate( + lines=data.splitlines(), + module=self._module, + ) + obj = vrf_address_family_parser.parse() + objs = list(obj.values()) + + for vrf in objs: + af = vrf.get("address_families", {}) + if af: + self._post_parse(vrf) + else: + vrf["address_families"] = [] + + ansible_facts["ansible_network_resources"].pop("vrf_address_family", None) + + params = utils.remove_empties( + vrf_address_family_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vrf_address_family"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def _post_parse(self, af_data): + """Converts the intermediate data structure + to valid format as per argspec. + :param obj: dict + """ + af = af_data.get("address_families", {}) + if af: + af_data["address_families"] = list(af.values()) diff --git a/plugins/module_utils/network/iosxr/rm_templates/vrf_address_family.py b/plugins/module_utils/network/iosxr/rm_templates/vrf_address_family.py new file mode 100644 index 000000000..679cccfbb --- /dev/null +++ b/plugins/module_utils/network/iosxr/rm_templates/vrf_address_family.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 Vrf_address_family 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 Vrf_address_familyTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Vrf_address_familyTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "name", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "vrf {{ name }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + }, + }, + "shared": True, + }, + { + "name": "address_family", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + $""", re.VERBOSE, + ), + "setval": "address-family {{ afi }} {{ safi }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + }, + }, + }, + }, + }, + { + "name": "export.route_policy", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+export\sroute-policy\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "export route-policy {{ export.route_policy }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "export": { + "route_policy": "{{ export_route_policy }}", + }, + }, + }, + }, + }, + }, + { + "name": "export.route_target", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+export\sroute-target\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "export route-target {{ export.route_target }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "export": { + "route_target": "{{ export_route_target }}", + }, + }, + }, + }, + }, + }, + { + "name": "export.to.default_vrf.route_policy", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+export\sto\sdefault-vrf\sroute-policy\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "export to default-vrf route-policy {{ export.to.default_vrf.route_policy }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "export": { + "to": { + "default_vrf": { + "route_policy": "{{ export_to_default_vrf_route_policy }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "export.to.vrf.allow_imported_vpn", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+export\sto\svrf\s(?Pallow-imported-vpn) + $""", re.VERBOSE, + ), + "setval": "export to vrf allow-imported-vpn", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "export": { + "to": { + "vrf": { + "allow_imported_vpn": "{{ true if allow_imported_vpn is defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "import_config.route_target", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+import\sroute-target\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "import route-target {{ import_config.route_target }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "import_config": { + "route_target": "{{import_config_route_target}}", + }, + }, + }, + }, + }, + }, + { + "name": "import_config.route_policy", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+import\sroute-policy\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "import route-policy {{ import_config.route_policy }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "import_config": { + "route_policy": "{{import_config_route_policy}}", + }, + }, + }, + }, + }, + }, + { + "name": "import_config.from_config.bridge_domain.advertise_as_vpn", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+import\sfrom\sbridge-domain\s(?Padvertise-as-vpn) + $""", re.VERBOSE, + ), + "setval": "import from bridge-domain advertise-as-vpn", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "import_config": { + "from_config": { + "bridge_domain": { + "advertise_as_vpn": "{{ true if advertise_as_vpn is defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "import_config.from_config.default_vrf.route_policy", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+import\sfrom\sdefault-vrf\sroute-policy\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "import from default-vrf route-policy {{ import_config.from_config.default_vrf.route_policy }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "import_config": { + "from_config": { + "default_vrf": { + "route_policy": "{{ import_config_from_config_default_vrf_route_policy }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "import_config.from_config.vrf.advertise_as_vpn", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+import\sfrom\svrf\s(?Padvertise-as-vpn) + $""", re.VERBOSE, + ), + "setval": "import from vrf advertise-as-vpn", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "import_config": { + "from_config": { + "vrf": { + "advertise_as_vpn": "{{ true if advertise_as_vpn is defined }}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "maximum.prefix", + "getval": re.compile( + r""" + ^vrf\s(?P\S+) + (?P\s+address-family\s(?P\S+)\s(?P\S+)) + \s+maximum\sprefix\s(?P\d+) + $""", re.VERBOSE, + ), + "setval": "maximum prefix {{ maximum.prefix }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "address_families": { + '{{"address_families_" + afi + "_" + safi }}': { + "afi": "{{ afi}}", + "safi": "{{safi}}", + "maximum": { + "prefix": "{{ maximum_prefix }}", + }, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/modules/iosxr_vrf_address_family.py b/plugins/modules/iosxr_vrf_address_family.py new file mode 100644 index 000000000..0b1f58baf --- /dev/null +++ b/plugins/modules/iosxr_vrf_address_family.py @@ -0,0 +1,846 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for iosxr_vrf_address_family +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: iosxr_vrf_address_family +short_description: Resource module to configure VRF Address family. +description: + - This module configures and manages the attributes of VRF address family on Cisco IOS-XR devices. +version_added: 10.0.0 +author: Ruchi Pakhle (@Ruchip16) +notes: + - Tested against Cisco IOSXR Version 10.0.0 + - This module works with connection C(network_cli). See L(the IOS_XR Platform Options,../network/user_guide/platform_iosxr.html) + - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide ` + - For more information on using Ansible to manage Cisco devices see the `Cisco integration page `. +options: + config: + description: VRF address family configuration. + type: list + elements: dict + suboptions: + name: + description: Name of the VRF. + type: str + required: true + address_families: + description: Enable address family and enter its config mode - AFI/SAFI configuration + type: list + elements: dict + suboptions: + afi: + description: Address Family Identifier (AFI) + type: str + choices: ['ipv4', 'ipv6'] + safi: + description: Address Family modifier + type: str + choices: [ 'flowspec', 'multicast', 'unicast'] + export: + description: VRF export + type: dict + suboptions: + route_policy: &route_policy + description: Use route_policy for export + type: str + route_target: &route_target + description: Specify export route target extended communities. + type: str + to: + description: Export routes to a VRF + type: dict + suboptions: + default_vrf: &default_vrf + description: Export routes to default VRF + type: dict + suboptions: + route_policy: *route_policy + vrf: + description: Export routes to a VRF + type: dict + suboptions: + allow_imported_vpn: + description: Allow export of imported VPN routes to non-default VRF + type: bool + import_config: + description: VRF import + type: dict + suboptions: + route_policy: *route_policy + route_target: *route_target + from_config: + description: Import routes from a VRF + type: dict + suboptions: + bridge_domain: + description: VRF import + type: dict + suboptions: + advertise_as_vpn: &advertise_as_vpn + description: Advertise local EVPN imported routes to PEs + type: bool + default_vrf: *default_vrf + vrf: + description: Import routes from a VRF + type: dict + suboptions: + advertise_as_vpn: *advertise_as_vpn + maximum: + description: Set maximum prefix limit + type: dict + suboptions: + prefix: + description: Set table's maximum prefix limit. + type: int + 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-XR device by + executing the command B(show running-config vrf). + - 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: + choices: [parsed, gathered, deleted, merged, replaced, rendered, overridden] + default: merged + description: + - The state the configuration should be left in + - The states I(rendered), I(gathered) and I(parsed) does not perform any change + on the device. + - The state I(rendered) will transform the configuration in C(config) option to + platform specific CLI commands which will be returned in the I(rendered) key + within the result. For state I(rendered) active connection to remote host is + not required. + - The state I(gathered) will fetch the running configuration from device and transform + it into structured data in the format as per the resource module argspec and + the value is returned in the I(gathered) key within the result. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into JSON format as per the resource module parameters and the + value is returned in the I(parsed) key within the result. The value of C(running_config) + option should be the same format as the output of command I(show running-config vrf). + connection to remote host is not required. + type: str +""" + +EXAMPLES = """ + +# Using merged +# +# Before state: +# ------------- +# +# RP/0/0/CPU0:iosxr#show running-config vrf +# vrf test +# + +- name: Merge provided configuration with device configuration + cisco.iosxr.iosxr_vrf_address_family: + config: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + state: merged + +# Task Output: +# ------------ +# +# before: [] +# +# commands: +# - vrf VRF4 +# - address-family ipv4 unicast +# - export route-policy rm-policy +# - export route-target 192.0.2.1:400 +# - export to default-vrf route-policy rm-policy +# - export to vrf allow-imported-vpn +# - import route-target 192.0.2.6:200 +# - import route-policy test-policy +# - import from bridge-domain advertise-as-vpn +# - import from default-vrf route-policy test-policy +# - import from vrf advertise-as-vpn +# - maximum prefix 100 +# +# after: +# - name: VRF4 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.1:400" +# route_policy: "rm-policy" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: true +# import_config: +# route_target: "192.0.2.6:200" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: true +# maximum: +# prefix: 100 +# +# After state: +# ------------ +# +# RP/0/0/CPU0:iosxr#show running-config vrf +# vrf VRF4 +# address-family ipv4 unicast +# export route-policy rm-policy +# export route-target 192.0.2.1:400 +# export to default-vrf route-policy rm-policy +# export to vrf allow-imported-vpn +# import route-target 192.0.2.6:200 +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# maximum prefix 100 + +# Using replaced +# +# Before state: +# ------------- +# +# RP/0/0/CPU0:iosxr#show running-config vrf +# vrf VRF4 +# address-family ipv4 unicast +# export route-policy rm-policy +# export route-target 192.0.2.1:400 +# export to default-vrf route-policy rm-policy +# export to vrf allow-imported-vpn +# import route-target 192.0.2.6:200 +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# maximum prefix 100 + +- name: Replace the provided configuration with the existing running configuration + cisco.iosxr.iosxr_vrf_address_family: + config: + - name: VRF7 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.2:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.4:400" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 200 + state: replaced + +# Task Output: +# ------------ +# +# - name: VRF4 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.1:400" +# route_policy: "rm-policy" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: true +# import_config: +# route_target: "192.0.2.6:200" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: true +# maximum: +# prefix: 100 +# +# commands: +# - vrf VRF7 +# - address-family ipv4 unicast +# - export route-policy rm-policy +# - export route-target 192.0.2.2:400 +# - export to default-vrf route-policy rm-policy +# - export to vrf allow-imported-vpn +# - import route-target 192.0.2.4:400 +# - import route-policy test-policy +# - import from bridge-domain advertise-as-vpn +# - import from default-vrf route-policy test-policy +# - import from vrf advertise-as-vpn +# - maximum prefix 200 +# +# after: +# - name: VRF7 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.2:400" +# route_policy: "rm-policy" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: true +# import_config: +# route_target: "192.0.2.4:400" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: true +# maximum: +# prefix: 200 +# +# After state: +# ------------ +# +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF7 +# address-family ipv4 unicast +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# import route-target +# 192.0.2.4:400 +# ! +# export route-policy rm-policy +# export to vrf allow-imported-vpn +# export to default-vrf route-policy rm-policy +# export route-target +# 192.0.2.2:400 +# ! +# maximum prefix 200 + +# Using overridden +# +# Before state: +# ------------- +# +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF7 +# address-family ipv4 unicast +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# import route-target +# 192.0.2.4:400 +# ! +# export route-policy rm-policy +# export to vrf allow-imported-vpn +# export to default-vrf route-policy rm-policy +# export route-target +# 192.0.2.2:400 +# ! +# maximum prefix 200 + +- name: Override the provided configuration with the existing running configuration + cisco.iosxr.iosxr_vrf_address_family: + state: overridden + config: + - name: VRF6 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.8:200" + route_policy: "rm-policy1" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: "true" + import_config: + route_target: "192.0.2.2:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: "true" + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: "true" + maximum: + prefix: 500 +# Task Output: +# ------------ +# +# before: +# - name: VRF7 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.2:400" +# route_policy: "rm-policy" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: true +# import_config: +# route_target: "192.0.2.4:400" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: true +# maximum: +# prefix: 200 +# +# commands: +# - vrf VRF7 +# - address-family ipv4 unicast +# - no import route-policy test-policy +# - no import from bridge-domain advertise-as-vpn +# - no import from default-vrf route-policy test-policy +# - no import from vrf advertise-as-vpn +# - no import route-target 192.0.2.4:400 +# - no export route-policy rm-policy +# - no export route-target 192.0.2.2:400 +# - no export to default-vrf route-policy rm-policy +# - no export to vrf allow-imported-vpn +# - no maximum prefix 200 +# - vrf VRF6 +# - address-family ipv4 unicast +# - export route-policy rm-policy1 +# - export route-target 192.0.2.8:200 +# - export to default-vrf route-policy rm-policy +# - export to vrf allow-imported-vpn +# - import route-target 192.0.2.2:200 +# - import route-policy test-policy +# - import from bridge-domain advertise-as-vpn +# - import from default-vrf route-policy test-policy +# - import from vrf advertise-as-vpn +# - maximum prefix 500 +# +# after: +# - name: VRF4 +# - name: VRF6 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.8:200" +# route_policy: "rm-policy1" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: "true" +# import_config: +# route_target: "192.0.2.2:200" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: "true" +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: "true" +# maximum: +# prefix: 500 +# - name: VRF7 +# +# After state: +# ------------- +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF4 +# vrf VRF6 +# address-family ipv4 unicast +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# import route-target +# 192.0.2.2:200 +# export route-policy rm-policy1 +# export to vrf allow-imported-vpn +# export to default-vrf route-policy rm-policy +# export route-target +# 192.0.2.8:200 +# maximum prefix 500 +# vrf VRF7 + +# Using deleted +# +# Before state: +# ------------- +# +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF4 +# vrf VRF6 +# address-family ipv4 unicast +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# import route-target +# 192.0.2.2:200 +# export route-policy rm-policy1 +# export to vrf allow-imported-vpn +# export to default-vrf route-policy rm-policy +# export route-target +# 192.0.2.8:200 +# maximum prefix 500 +# vrf VRF7 + +- name: Delete the provided configuration + cisco.iosxr.iosxr_vrf_address_family: + config: + state: deleted + +# Task Output: +# ------------ +# +# before: +# - name: VRF4 +# - name: VRF6 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.8:200" +# route_policy: "rm-policy1" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: "true" +# import_config: +# route_target: "192.0.2.2:200" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: "true" +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: "true" +# maximum: +# prefix: 500 +# - name: VRF7 + +# commands: +# - vrf VRF4 +# - vrf VRF6 +# - no address-family ipv4 unicast +# - vrf VRF7 +# +# after: +# - name: VRF4 +# - name: VRF6 +# - name: VRF7 +# +# After state: +# ------------ +# +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF4 +# vrf VRF6 +# vrf VRF7 + +# Using rendered +# +- name: Render provided configuration with device configuration + cisco.iosxr.iosxr_vrf_address_family: + config: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + state: rendered + +# Task Output: +# ------------ +# +# rendered: +# - vrf VRF4 +# - address-family ipv4 unicast +# - export route-policy rm-policy +# - export route-target 192.0.2.1:400 +# - export to default-vrf route-policy rm-policy +# - export to vrf allow-imported-vpn +# - import route-target 192.0.2.6:200 +# - import route-policy test-policy +# - import from bridge-domain advertise-as-vpn +# - import from default-vrf route-policy test-policy +# - import from vrf advertise-as-vpn +# - maximum prefix 100 + +# Using gathered +# +# Before state: +# ------------- +# +# RP/0/RP0/CPU0:iosxr(config)#show running-config vrf +# vrf VRF4 +# address-family ipv4 unicast +# export route-policy rm-policy +# export route-target 192.0.2.1:400 +# export to default-vrf route-policy rm-policy +# export to vrf allow-imported-vpn +# import route-target 192.0.2.6:200 +# import route-policy test-policy +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy test-policy +# import from vrf advertise-as-vpn +# maximum prefix 100 + +- name: Gather existing running configuration + cisco.iosxr.iosxr_vrf_address_family: + state: gathered + +# Task Output: +# ------------ +# +# gathered: +# - name: VRF4 +# address_families: +# - afi: "ipv4" +# safi: "unicast" +# export: +# route_target: "192.0.2.1:400" +# route_policy: "rm-policy" +# to: +# default_vrf: +# route_policy: "rm-policy" +# vrf: +# allow_imported_vpn: true +# import_config: +# route_target: "192.0.2.6:200" +# route_policy: "test-policy" +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: "test-policy" +# vrf: +# advertise_as_vpn: true +# maximum: +# prefix: 100 + +# Using parsed +# +# File: parsed.cfg +# ---------------- +# +# vrf test +# address-family ipv4 unicast +# export to default-vrf route-policy "rm-policy" +# export to vrf allow-imported-vpn +# export route-policy "export-policy" +# export route-target +# 192.0.2.1:400 +# import route-target +# 192.0.2.2:200 +# import route-policy "test-policy" +# import from bridge-domain advertise-as-vpn +# import from default-vrf route-policy "new-policy" +# import from vrf advertise-as-vpn +# maximum prefix 23 + +- name: Parse the provided configuration + cisco.iosxr.iosxr_vrf_address_family: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task Output: +# ------------ +# +# parsed: +# - address_families: +# - afi: ipv4 +# export: +# route_policy: export-policy +# route_target: 192.0.2.1:400 +# to: +# default_vrf: +# route_policy: rm-policy +# vrf: +# allow_imported_vpn: true +# import_config: +# from_config: +# bridge_domain: +# advertise_as_vpn: true +# default_vrf: +# route_policy: new-policy +# vrf: +# advertise_as_vpn: true +# route_policy: test-policy +# route_target: 192.0.2.2:200 +# maximum: +# prefix: 23 +# safi: unicast +# name: test +""" + +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - vrf VRF7 + - address-family ipv4 unicast + - export route-policy rm-policy + - import route-policy test-policy +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - vrf VRF4 + - address-family ipv4 unicast + - export route-policy rm-policy +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.argspec.vrf_address_family.vrf_address_family import ( + Vrf_address_familyArgs, +) +from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.config.vrf_address_family.vrf_address_family import ( + Vrf_address_family, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vrf_address_familyArgs.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 = Vrf_address_family(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/iosxr_vrf_address_family/defaults/main.yaml b/tests/integration/targets/iosxr_vrf_address_family/defaults/main.yaml new file mode 100644 index 000000000..871ea460c --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "[^_].*" diff --git a/tests/integration/targets/iosxr_vrf_address_family/meta/main.yaml b/tests/integration/targets/iosxr_vrf_address_family/meta/main.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/iosxr_vrf_address_family/tasks/cli.yaml b/tests/integration/targets/iosxr_vrf_address_family/tasks/cli.yaml new file mode 100644 index 000000000..86ca0d9cb --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: Collect all CLI test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/common" + 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 }}" + +- 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 + tags: + - network_cli diff --git a/tests/integration/targets/iosxr_vrf_address_family/tasks/main.yaml b/tests/integration/targets/iosxr_vrf_address_family/tasks/main.yaml new file mode 100644 index 000000000..f75f2f031 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Include the CLI tasks + ansible.builtin.include_tasks: cli.yaml + tags: + - cli diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/_parsed.cfg b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_parsed.cfg new file mode 100644 index 000000000..aed148b6e --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_parsed.cfg @@ -0,0 +1,15 @@ +vrf test + address-family ipv4 unicast + export to default-vrf route-policy "rm-policy" + export to vrf allow-imported-vpn + export route-policy "export-policy" + export route-target + 192.0.2.1:400 + import route-target + 192.0.2.2:200 + import route-policy "test-policy" + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy "new-policy" + import from vrf advertise-as-vpn + maximum prefix 23 + ! diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/_populate.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_populate.yaml new file mode 100644 index 000000000..f90ffc36e --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_populate.yaml @@ -0,0 +1,17 @@ +--- +- name: Merge provided configuration with device configuration + register: result + cisco.iosxr.iosxr_config: + lines: + - vrf VRF4 + - address-family ipv4 unicast + - export route-policy rm-policy + - export route-target 192.0.2.1:400 + - export to default-vrf route-policy rm-policy + - export to vrf allow-imported-vpn + - import route-target 192.0.2.6:200 + - import route-policy test-policy + - import from bridge-domain advertise-as-vpn + - import from default-vrf route-policy test-policy + - import from vrf advertise-as-vpn + - maximum prefix 100 diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/_remove_config.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_remove_config.yaml new file mode 100644 index 000000000..b6fe68617 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/_remove_config.yaml @@ -0,0 +1,9 @@ +--- +- name: Remove VRF global configurations + cisco.iosxr.iosxr_config: + lines: + - no vrf VRF4 + - no vrf VRF6 + - no vrf VRF7 + - no vrf test + register: result diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/deleted.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/deleted.yaml new file mode 100644 index 000000000..f2b1c84fd --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/deleted.yaml @@ -0,0 +1,36 @@ +--- +- ansible.builtin.debug: + msg: Start iosxr_vrf_address_family deleted integration tests connection={{ ansible_connection}} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate.yaml + +- block: + - name: Delete given vrf configuration + register: result + cisco.iosxr.iosxr_vrf_address_family: &deleted + config: + state: deleted + + - ansible.builtin.assert: + that: + - result.changed == true + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ deleted['commands'] == result['commands'] }}" + + - name: Assert that after dicts are correctly generated + ansible.builtin.assert: + that: + - deleted['after'] == result['after'] + + - name: Idempotency check + register: result + cisco.iosxr.iosxr_vrf_address_family: *deleted + - ansible.builtin.assert: + that: + - result.changed == false + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/empty_config.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/empty_config.yaml new file mode 100644 index 000000000..b52a28841 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/empty_config.yaml @@ -0,0 +1,57 @@ +--- +- ansible.builtin.debug: + msg: START iosxr_vrf_address_family empty_config integration tests on connection={{ ansible_connection }} + +- name: Merged with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.iosxr.iosxr_vrf_address_family: + 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.iosxr.iosxr_vrf_address_family: + 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_vrf_address_family: + 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.iosxr.iosxr_vrf_address_family: + 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.iosxr.iosxr_vrf_address_family: + config: + state: parsed + +- ansible.builtin.debug: + msg: END iosxr_vrf_address_family empty_config integration tests on connection={{ ansible_connection }} diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/gathered.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/gathered.yaml new file mode 100644 index 000000000..9e2ba7863 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/gathered.yaml @@ -0,0 +1,22 @@ +--- +- ansible.builtin.debug: + msg: START iosxr_vrf_address_family gathered integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- ansible.builtin.include_tasks: _populate.yaml + +- block: + - name: Gather existing running configuration + register: result + cisco.iosxr.iosxr_vrf_address_family: + config: + state: gathered + + - name: Assert + ansible.builtin.assert: + that: + - result.changed == false + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/merged.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/merged.yaml new file mode 100644 index 000000000..e56125c00 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/merged.yaml @@ -0,0 +1,63 @@ +--- +- ansible.builtin.debug: + msg: Start iosxr_vrf_address_family merged integration tests connection={{ ansible_connection}} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- block: + - name: Merge provided configuration with device configuration + cisco.iosxr.iosxr_vrf_address_family: &merged + config: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + state: merged + register: result + + - 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'] == {} + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - merged['after'] == result['after'] + + - name: Merge the provided configuration with the existing running configuration (idempotent) + cisco.iosxr.iosxr_vrf_address_family: *merged + register: result + + - 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/iosxr_vrf_address_family/tests/common/overridden.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/overridden.yaml new file mode 100644 index 000000000..91a3bbab7 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/overridden.yaml @@ -0,0 +1,71 @@ +--- +- ansible.builtin.debug: + msg: START iosxr_vrf_address_family overridden integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _populate.yaml + +- ansible.builtin.include_tasks: _remove_config.yaml + +- block: + - name: Override the provided configuration with the existing running configuration + cisco.iosxr.iosxr_vrf_address_family: &overridden + config: + - name: VRF7 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.8:200" + route_policy: "rm-policy1" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.2:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 500 + state: overridden + register: result + + - 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'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Idempotency check + cisco.iosxr.iosxr_vrf_address_family: *overridden + register: result + + - name: Assert that no changes were made + ansible.builtin.assert: + that: + - result['changed'] == false + - result.commands|length == 0 + + - name: Assert that before dict is correctly generated + ansible.builtin.assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/parsed.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/parsed.yaml new file mode 100644 index 000000000..5210080c2 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/parsed.yaml @@ -0,0 +1,14 @@ +--- +- ansible.builtin.debug: + msg: START iosxr_vrf_address_family parsed integration tests on connection={{ ansible_connection }} + +- name: Parse externally provided VRF configuration + register: result + cisco.iosxr.iosxr_vrf_address_family: + running_config: "{{ lookup('file', './_parsed.cfg') }}" + state: parsed + +- ansible.builtin.assert: + that: + - result.changed == false + - parsed['after'] == result['parsed'] diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/rendered.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/rendered.yaml new file mode 100644 index 000000000..82ed1b651 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/rendered.yaml @@ -0,0 +1,41 @@ +--- +- ansible.builtin.debug: + msg: START iosxr_vrf_address_family rendered integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- name: Render provided configuration with device configuration + cisco.iosxr.iosxr_vrf_address_family: + config: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: "true" + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: "true" + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: "true" + maximum: + prefix: 100 + state: rendered + register: result + +- name: Assert that correct set of commands were rendered + ansible.builtin.assert: + that: + - result.changed == false + - result.rendered|symmetric_difference(merged.commands) == [] diff --git a/tests/integration/targets/iosxr_vrf_address_family/tests/common/replaced.yaml b/tests/integration/targets/iosxr_vrf_address_family/tests/common/replaced.yaml new file mode 100644 index 000000000..4cd4e1a60 --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/tests/common/replaced.yaml @@ -0,0 +1,66 @@ +--- +- ansible.builtin.debug: + msg: Start iosxr_vrf_address_family replaced integration tests connection={{ ansible_connection}} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- ansible.builtin.include_tasks: _populate.yaml + +- block: + - name: Replace given vrf configuration with provided configurations + cisco.iosxr.iosxr_vrf_address_family: &replaced + config: + - name: VRF6 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.0:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: "true" + import_config: + route_target: "192.0.2.3:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: "true" + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: "true" + state: replaced + register: result + + - 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 dict is 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: Idempotency check + register: result + cisco.iosxr.iosxr_vrf_address_family: *replaced + - ansible.builtin.assert: + that: + - result['changed'] == false + - result.commands|length == 0 + + - name: Assert that before dict is correctly generated + ansible.builtin.assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/iosxr_vrf_address_family/vars/main.yaml b/tests/integration/targets/iosxr_vrf_address_family/vars/main.yaml new file mode 100644 index 000000000..d48e02eae --- /dev/null +++ b/tests/integration/targets/iosxr_vrf_address_family/vars/main.yaml @@ -0,0 +1,263 @@ +--- +merged: + before: {} + commands: + - vrf VRF4 + - address-family ipv4 unicast + - export route-policy rm-policy + - export route-target 192.0.2.1:400 + - export to default-vrf route-policy rm-policy + - export to vrf allow-imported-vpn + - import route-target 192.0.2.6:200 + - import route-policy test-policy + - import from bridge-domain advertise-as-vpn + - import from default-vrf route-policy test-policy + - import from vrf advertise-as-vpn + - maximum prefix 100 + after: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + - name: VRF5 + +replaced: + before: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + - name: VRF5 + + commands: + - vrf VRF6 + - address-family ipv4 unicast + - export route-policy rm-policy + - export route-target 192.0.2.0:400 + - export to default-vrf route-policy rm-policy + - export to vrf allow-imported-vpn + - import route-target 192.0.2.3:200 + - import route-policy test-policy + - import from bridge-domain advertise-as-vpn + - import from default-vrf route-policy test-policy + - import from vrf advertise-as-vpn + + after: + - name: VRF4 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 + - name: VRF5 + - name: VRF6 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.0:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.3:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + +overridden: + before: + - name: VRF5 + commands: + - vrf VRF7 + - address-family ipv4 unicast + - export route-policy rm-policy1 + - export route-target 192.0.2.8:200 + - export to default-vrf route-policy rm-policy + - export to vrf allow-imported-vpn + - import route-target 192.0.2.2:200 + - import route-policy test-policy + - import from bridge-domain advertise-as-vpn + - import from default-vrf route-policy test-policy + - import from vrf advertise-as-vpn + - maximum prefix 500 + after: + - name: VRF5 + - name: VRF7 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.8:200" + route_policy: "rm-policy1" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.2:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 500 + +gathered: + after: + - name: VRF4 + - name: VRF5 + address_families: + - afi: "ipv4" + safi: "unicast" + export: + route_target: "192.0.2.1:400" + route_policy: "rm-policy" + to: + default_vrf: + route_policy: "rm-policy" + vrf: + allow_imported_vpn: true + import_config: + route_target: "192.0.2.6:200" + route_policy: "test-policy" + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: "test-policy" + vrf: + advertise_as_vpn: true + maximum: + prefix: 100 +parsed: + after: + - name: test + address_families: + - afi: ipv4 + export: + route_policy: export-policy + route_target: 192.0.2.1:400 + to: + default_vrf: + route_policy: rm-policy + vrf: + allow_imported_vpn: true + import_config: + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: new-policy + vrf: + advertise_as_vpn: true + route_policy: test-policy + route_target: 192.0.2.2:200 + maximum: + prefix: 23 + safi: unicast + +deleted: + before: + - name: VRF4 + - name: VRF7 + address_families: + - afi: ipv4 + export: + route_policy: rm-policy1 + route_target: 192.0.2.8:200 + to: + default_vrf: + route_policy: rm-policy + vrf: + allow_imported_vpn: true + import_config: + from_config: + bridge_domain: + advertise_as_vpn: true + default_vrf: + route_policy: test-policy + vrf: + advertise_as_vpn: true + route_policy: test-policy + route_target: 192.0.2.2:200 + maximum: + prefix: 500 + safi: unicast + commands: + - vrf VRF4 + - no address-family ipv4 unicast + after: + - name: VRF4 + - name: VRF5 diff --git a/tests/unit/modules/network/iosxr/test_iosxr_vrf_address_family.py b/tests/unit/modules/network/iosxr/test_iosxr_vrf_address_family.py new file mode 100644 index 000000000..5eb6038af --- /dev/null +++ b/tests/unit/modules/network/iosxr/test_iosxr_vrf_address_family.py @@ -0,0 +1,611 @@ +# (c) 2021 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from textwrap import dedent +from unittest.mock import patch + +from ansible_collections.cisco.iosxr.plugins.modules import iosxr_vrf_address_family +from ansible_collections.cisco.iosxr.tests.unit.modules.utils import set_module_args + +from .iosxr_module import TestIosxrModule + + +class TestIosxrVrfAddressFamilyModule(TestIosxrModule): + """Tests the iosxr_vrf_address_family module.""" + + module = iosxr_vrf_address_family + + def setUp(self): + """Setup for iosxr_vrf_address_family module tests.""" + super(TestIosxrVrfAddressFamilyModule, self).setUp() + + self.mock_get_resource_connection = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base." + "get_resource_connection", + ) + self.get_resource_connection = self.mock_get_resource_connection.start() + + self.mock_get_config = patch( + "ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.vrf_address_family.vrf_address_family." + "Vrf_address_familyFacts.get_config", + ) + self.get_config = self.mock_get_config.start() + + def tearDown(self): + """Tear down for iosxr_vrf_address_family module tests.""" + super(TestIosxrVrfAddressFamilyModule, self).tearDown() + self.get_resource_connection.stop() + self.get_config.stop() + + def test_iosxr_vrf_address_family_merged_idempotent(self): + """Test the idempotent nature of the iosxr_vrf_address_family module in merged state.""" + run_cfg = dedent( + """\ + vrf test + address-family ipv4 unicast + export to default-vrf route-policy "rm-policy" + export to vrf allow-imported-vpn + export route-policy "export-policy" + export route-target + 192.0.2.1:400 + import route-target + 192.0.2.2:200 + import route-policy "test-policy" + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy "new-policy" + import from vrf advertise-as-vpn + maximum prefix 23 + """, + ) + self.get_config.return_value = run_cfg + set_module_args( + dict( + config=[ + dict( + name="test", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="export-policy", + route_target="192.0.2.1:400", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + route_policy="test-policy", + route_target="192.0.2.2:200", + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="new-policy"), + vrf=dict(advertise_as_vpn=True), + ), + ), + maximum=dict(prefix=23), + ), + ], + ), + ], + state="merged", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_vrf_address_family_merged(self): + """Test the merged state of the iosxr_vrf_address_family module.""" + set_module_args( + dict( + config=[ + dict( + name="VRF4", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy", + route_target="192.0.2.1:400", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.6:200", + ), + maximum=dict(prefix=100), + ), + ], + ), + ], + state="merged", + ), + ) + commands = [ + "vrf VRF4", + "address-family ipv4 unicast", + "export route-policy rm-policy", + "export route-target 192.0.2.1:400", + "export to default-vrf route-policy rm-policy", + "export to vrf allow-imported-vpn", + "import route-target 192.0.2.6:200", + "import route-policy test-policy", + "import from bridge-domain advertise-as-vpn", + "import from default-vrf route-policy test-policy", + "import from vrf advertise-as-vpn", + "maximum prefix 100", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_iosxr_vrf_address_family_replaced(self): + """Test the replaced state of the iosxr_vrf_address_family module.""" + run_cfg = dedent( + """\ + vrf VRF4 + address-family ipv4 unicast + import route-policy test-policy + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy test-policy + import from vrf advertise-as-vpn + import route-target + 192.0.2.6:200 + ! + export route-policy rm-policy + export to vrf allow-imported-vpn + export to default-vrf route-policy rm-policy + export route-target + 192.0.2.1:400 + ! + maximum prefix 100 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF7", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy", + route_target="192.0.2.2:400", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.4:400", + ), + maximum=dict(prefix=200), + ), + ], + ), + ], + state="replaced", + ), + ) + commands = [ + "vrf VRF7", + "address-family ipv4 unicast", + "export route-policy rm-policy", + "export route-target 192.0.2.2:400", + "export to default-vrf route-policy rm-policy", + "export to vrf allow-imported-vpn", + "import route-target 192.0.2.4:400", + "import route-policy test-policy", + "import from bridge-domain advertise-as-vpn", + "import from default-vrf route-policy test-policy", + "import from vrf advertise-as-vpn", + "maximum prefix 200", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_iosxr_vrf_address_family_replaced_idempotent(self): + """Test the idempotent nature of the iosxr_vrf_address_family module in replaced state.""" + run_cfg = dedent( + """\ + vrf VRF7 + address-family ipv4 unicast + import route-policy test-policy + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy test-policy + import from vrf advertise-as-vpn + import route-target + 192.0.2.4:400 + ! + export route-policy rm-policy + export to vrf allow-imported-vpn + export to default-vrf route-policy rm-policy + export route-target + 192.0.2.2:400 + ! + maximum prefix 200 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF7", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy", + route_target="192.0.2.2:400", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.4:400", + ), + maximum=dict(prefix=200), + ), + ], + ), + ], + state="replaced", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_vrf_address_family_overridden(self): + """Test the overridden state of the iosxr_vrf_address_family module.""" + run_cfg = dedent( + """\ + vrf VRF7 + address-family ipv4 unicast + import route-policy test-policy + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy test-policy + import from vrf advertise-as-vpn + import route-target + 192.0.2.4:400 + ! + export route-policy rm-policy + export to vrf allow-imported-vpn + export to default-vrf route-policy rm-policy + export route-target + 192.0.2.2:400 + ! + maximum prefix 200 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF6", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy1", + route_target="192.0.2.8:200", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.2:200", + ), + maximum=dict(prefix=500), + ), + ], + ), + ], + state="overridden", + ), + ) + commands = [ + "vrf VRF7", + "address-family ipv4 unicast", + "no import route-policy test-policy", + "no import from bridge-domain advertise-as-vpn", + "no import from default-vrf route-policy test-policy", + "no import from vrf advertise-as-vpn", + "no import route-target 192.0.2.4:400", + "no export route-policy rm-policy", + "no export route-target 192.0.2.2:400", + "no export to default-vrf route-policy rm-policy", + "no export to vrf allow-imported-vpn", + "no maximum prefix 200", + "vrf VRF6", + "address-family ipv4 unicast", + "export route-policy rm-policy1", + "export route-target 192.0.2.8:200", + "export to default-vrf route-policy rm-policy", + "export to vrf allow-imported-vpn", + "import route-target 192.0.2.2:200", + "import route-policy test-policy", + "import from bridge-domain advertise-as-vpn", + "import from default-vrf route-policy test-policy", + "import from vrf advertise-as-vpn", + "maximum prefix 500", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_iosxr_vrf_address_family_overridden_idempotent(self): + """Test the idempotent nature of the iosxr_vrf_address_family module in overridden state.""" + run_cfg = dedent( + """\ + vrf VRF6 + address-family ipv4 unicast + import route-policy test-policy + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy test-policy + import from vrf advertise-as-vpn + import route-target + 192.0.2.2:200 + ! + export route-policy rm-policy1 + export to vrf allow-imported-vpn + export to default-vrf route-policy rm-policy + export route-target + 192.0.2.8:200 + ! + maximum prefix 500 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF6", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy1", + route_target="192.0.2.8:200", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.2:200", + ), + maximum=dict(prefix=500), + ), + ], + ), + ], + state="overridden", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_vrf_address_family_deleted(self): + """Test the deleted state of the iosxr_vrf_address_family module.""" + run_cfg = dedent( + """\ + vrf VRF7 + address-family ipv4 unicast + import route-policy test-policy + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy test-policy + import from vrf advertise-as-vpn + import route-target + 192.0.2.2:200 + ! + export route-policy rm-policy + export to vrf allow-imported-vpn + export to default-vrf route-policy rm-policy + export route-target + 192.12.3.2:300 + ! + maximum prefix 200 + """, + ) + self.get_config.return_value = run_cfg + set_module_args( + dict( + config=[ + dict( + name="VRF7", + ), + ], + state="deleted", + ), + ) + + commands = [ + "vrf VRF7", + "no address-family ipv4 unicast", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_iosxr_vrf_address_family_deleted_idempotent(self): + """Test the idempotent nature of the iosxr_vrf_address_family module in deleted state.""" + run_cfg = dedent( + """\ + """, + ) + self.get_config.return_value = run_cfg + set_module_args(dict(config=[], state="deleted")) + + result = self.execute_module(changed=False) + self.assertEqual(result["commands"], []) + + def test_iosxr_vrf_address_family_rendered(self): + """Test the rendered state of the iosxr_vrf_address_family module.""" + set_module_args( + dict( + config=[ + dict( + name="VRF4", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + export=dict( + route_policy="rm-policy", + route_target="192.0.2.1:400", + to=dict( + default_vrf=dict(route_policy="rm-policy"), + vrf=dict(allow_imported_vpn=True), + ), + ), + import_config=dict( + from_config=dict( + bridge_domain=dict(advertise_as_vpn=True), + default_vrf=dict(route_policy="test-policy"), + vrf=dict(advertise_as_vpn=True), + ), + route_policy="test-policy", + route_target="192.0.2.6:200", + ), + maximum=dict(prefix=100), + ), + ], + ), + ], + state="rendered", + ), + ) + commands = [ + "vrf VRF4", + "address-family ipv4 unicast", + "export route-policy rm-policy", + "export route-target 192.0.2.1:400", + "export to default-vrf route-policy rm-policy", + "export to vrf allow-imported-vpn", + "import route-target 192.0.2.6:200", + "import route-policy test-policy", + "import from bridge-domain advertise-as-vpn", + "import from default-vrf route-policy test-policy", + "import from vrf advertise-as-vpn", + "maximum prefix 100", + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result["rendered"]), sorted(commands)) + + def test_iosxr_vrf_address_family_parsed(self): + """Test the parsed state of the iosxr_vrf_address_family module.""" + run_cfg = dedent( + """\ + vrf test + address-family ipv4 unicast + export to default-vrf route-policy "rm-policy" + export to vrf allow-imported-vpn + export route-policy "export-policy" + export route-target + 192.0.2.1:400 + import route-target + 192.0.2.2:200 + import route-policy "test-policy" + import from bridge-domain advertise-as-vpn + import from default-vrf route-policy "new-policy" + import from vrf advertise-as-vpn + maximum prefix 23 + """, + ) + set_module_args(dict(running_config=run_cfg, state="parsed")) + result = self.execute_module(changed=False) + parsed_list = [ + { + "name": "test", + "address_families": [ + { + "afi": "ipv4", + "safi": "unicast", + "import_config": { + "route_policy": "test-policy", + "route_target": "192.0.2.2:200", + "from_config": { + "bridge_domain": {"advertise_as_vpn": True}, + "default_vrf": {"route_policy": "new-policy"}, + "vrf": {"advertise_as_vpn": True}, + }, + }, + "export": { + "route_policy": "export-policy", + "route_target": "192.0.2.1:400", + "to": { + "default_vrf": {"route_policy": "rm-policy"}, + "vrf": {"allow_imported_vpn": True}, + }, + }, + "maximum": {"prefix": 23}, + }, + ], + }, + ] + + self.assertEqual(parsed_list, result["parsed"])