diff --git a/.gitignore b/.gitignore index 894a44cc0..7d3392b09 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ venv.bak/ # mypy .mypy_cache/ +.vscode diff --git a/README.md b/README.md index 41fc1887a..20cdea4b3 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Name | Description [arista.eos.eos_user](https://github.com/ansible-collections/arista.eos/blob/main/docs/arista.eos.eos_user_module.rst)|Manage the collection of local users on EOS devices [arista.eos.eos_vlans](https://github.com/ansible-collections/arista.eos/blob/main/docs/arista.eos.eos_vlans_module.rst)|VLANs resource module [arista.eos.eos_vrf](https://github.com/ansible-collections/arista.eos/blob/main/docs/arista.eos.eos_vrf_module.rst)|Manage VRFs on Arista EOS network devices +[arista.eos.eos_vrf_global](https://github.com/ansible-collections/arista.eos/blob/main/docs/arista.eos.eos_vrf_global_module.rst)|Resource module to configure VRF definitions. diff --git a/changelogs/fragments/add_vrf_global.yaml b/changelogs/fragments/add_vrf_global.yaml new file mode 100644 index 000000000..a14c7c46a --- /dev/null +++ b/changelogs/fragments/add_vrf_global.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Adds a new module `eos_vrf_global` in favor of `eos_vrf` legacy module to manage VRF global configurations on Arista EOS devices. diff --git a/meta/runtime.yml b/meta/runtime.yml index 5fef4d880..5bb06b358 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -66,3 +66,5 @@ plugin_routing: redirect: arista.eos.eos_vlans vrf: redirect: arista.eos.eos_vrf + vrf_global: + redirect: arista.eos.eos_vrf_global diff --git a/plugins/action/vrf_global.py b/plugins/action/vrf_global.py new file mode 120000 index 000000000..6496bc37b --- /dev/null +++ b/plugins/action/vrf_global.py @@ -0,0 +1 @@ +eos.py \ No newline at end of file diff --git a/plugins/module_utils/network/eos/argspec/vrf_global/__init__.py b/plugins/module_utils/network/eos/argspec/vrf_global/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/eos/argspec/vrf_global/vrf_global.py b/plugins/module_utils/network/eos/argspec/vrf_global/vrf_global.py new file mode 100644 index 000000000..8e26e0fc3 --- /dev/null +++ b/plugins/module_utils/network/eos/argspec/vrf_global/vrf_global.py @@ -0,0 +1,60 @@ +# -*- 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 eos_vrf_global module +""" + + +class Vrf_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the eos_vrf_global module""" + + argument_spec = { + "config": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "description": {"type": "str"}, + "rd": {"type": "str"}, + }, + }, + "running_config": {"type": "str"}, + "state": { + "choices": [ + "parsed", + "gathered", + "deleted", + "merged", + "replaced", + "rendered", + "overridden", + "purged", + ], + "default": "merged", + "type": "str", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/eos/config/vrf_global/__init__.py b/plugins/module_utils/network/eos/config/vrf_global/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/eos/config/vrf_global/vrf_global.py b/plugins/module_utils/network/eos/config/vrf_global/vrf_global.py new file mode 100644 index 000000000..6dfc229b8 --- /dev/null +++ b/plugins/module_utils/network/eos/config/vrf_global/vrf_global.py @@ -0,0 +1,118 @@ +# +# -*- 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 eos_vrf_global 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.arista.eos.plugins.module_utils.network.eos.facts.facts import Facts +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.vrf_global import ( + Vrf_globalTemplate, +) + + +class Vrf_global(ResourceModule): + """ + The eos_vrf_global config class + """ + + def __init__(self, module): + super(Vrf_global, self).__init__( + empty_fact_val=[], + facts_module=Facts(module), + module=module, + resource="vrf_global", + tmplt=Vrf_globalTemplate(), + ) + self.parsers = [ + "description", + "rd", + ] + + 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 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 = {} + + # remove superfluous config for overridden and deleted + if self.state in ["overridden", "deleted"]: + for k, have in iteritems(haved): + if k not in wantd: + self._compare(want={}, have=have, vrf=k) + + if self.state == "purged": + for k, have in iteritems(haved): + self.purge(have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {}), vrf=k) + + def _compare(self, want, have, vrf): + """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. + """ + begin = len(self.commands) + self.compare(self.parsers, want=want, have=have) + if len(self.commands) != begin: + self.commands.insert(begin, self._tmplt.render({"name": vrf}, "name", False)) + + def _vrf_list_to_dict(self, entry): + """Convert list of items to dict of items + for efficient diff calculation. + :params entry: data dictionary + """ + entry = {x["name"]: x for x in entry} + return entry + + def purge(self, have): + """Purge the VRF configuration""" + self.commands.append("no vrf instance {0}".format(have["name"])) diff --git a/plugins/module_utils/network/eos/facts/facts.py b/plugins/module_utils/network/eos/facts/facts.py index 87fd402eb..c4e4db3ad 100644 --- a/plugins/module_utils/network/eos/facts/facts.py +++ b/plugins/module_utils/network/eos/facts/facts.py @@ -91,6 +91,9 @@ from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.vlans.vlans import ( VlansFacts, ) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.vrf_global.vrf_global import ( + Vrf_globalFacts, +) FACT_LEGACY_SUBSETS = dict( @@ -123,6 +126,7 @@ ntp_global=Ntp_globalFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, + vrf_global=Vrf_globalFacts, ) diff --git a/plugins/module_utils/network/eos/facts/vrf_global/__init__.py b/plugins/module_utils/network/eos/facts/vrf_global/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/eos/facts/vrf_global/vrf_global.py b/plugins/module_utils/network/eos/facts/vrf_global/vrf_global.py new file mode 100644 index 000000000..6dfeddd78 --- /dev/null +++ b/plugins/module_utils/network/eos/facts/vrf_global/vrf_global.py @@ -0,0 +1,69 @@ +# -*- 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 eos vrf_global 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.arista.eos.plugins.module_utils.network.eos.argspec.vrf_global.vrf_global import ( + Vrf_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.vrf_global import ( + Vrf_globalTemplate, +) + + +class Vrf_globalFacts(object): + """The eos vrf_global facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vrf_globalArgs.argument_spec + + def get_config(self, connection): + """Get the configuration from the device""" + + return connection.get("show running-config | section ^vrf") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vrf_global 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_config(connection) + + # parse native config using the Vrf_global template + vrf_global_parser = Vrf_globalTemplate(lines=data.splitlines(), module=self._module) + objs = list(vrf_global_parser.parse().values()) + + ansible_facts["ansible_network_resources"].pop("vrf_global", None) + + params = utils.remove_empties( + vrf_global_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + facts["vrf_global"] = params.get("config", []) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/eos/rm_templates/vrf_global.py b/plugins/module_utils/network/eos/rm_templates/vrf_global.py new file mode 100644 index 000000000..8ea092d9a --- /dev/null +++ b/plugins/module_utils/network/eos/rm_templates/vrf_global.py @@ -0,0 +1,79 @@ +# -*- 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_global 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_globalTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Vrf_globalTemplate, self).__init__(lines=lines, tmplt=self, module=module) + + # fmt: off + PARSERS = [ + { + "name": "name", + "getval": re.compile( + r""" + ^vrf\sinstance + (\s(?P\S+))? + $""", re.VERBOSE, + ), + "setval": "vrf instance {{ name }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + }, + }, + "shared": True, + }, + { + "name": "description", + "getval": re.compile( + r""" + \s+description\s(?P.+$) + $""", re.VERBOSE, + ), + "setval": "description {{ description }}", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + 'description': '{{ description }}', + }, + }, + }, + { + "name": "rd", + "getval": re.compile( + r""" + \s+rd\s(?P\S+) + $""", re.VERBOSE, + ), + "setval": "rd {{ rd }}", + "compval": "rd", + "result": { + '{{ name }}': { + 'name': '{{ name }}', + "rd": "{{ rd }}", + }, + }, + }, + ] + # fmt: on diff --git a/plugins/modules/eos_vrf_global.py b/plugins/modules/eos_vrf_global.py new file mode 100644 index 000000000..7f5d46729 --- /dev/null +++ b/plugins/modules/eos_vrf_global.py @@ -0,0 +1,461 @@ +#!/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 eos_vrf_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: eos_vrf_global +short_description: Resource module to configure VRF definitions. +description: This module provides declarative management of VRF definitions on Arista EOS platforms. +version_added: 10.0.0 +author: Ruchi Pakhle (@Ruchip16) +notes: +- Tested against Arista EOS 4.23.0F +- This module works with connection C(network_cli). See the L(EOS Platform Options,eos_platform_options). +options: + config: + description: A list of dictionaries containing device configurations for VRF definitions. + type: list + elements: dict + suboptions: + name: + description: Name of the VRF Instance. + type: str + required: true + description: + description: A description for the VRF. + type: str + rd: + description: BGP Route Distinguisher (RD). + 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 EOS 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, purged] + 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: +# ------------- +# +# test#show running-config | section ^vrf + +- name: Merge provided configuration with device configuration + arista.eos.eos_vrf_global: + config: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + state: merged + +# Task Output: +# ------------ +# +# before: [] +# +# commands: +# - vrf instance VRF4 +# - description VRF4 Description +# - rd 3:4 +# +# after: +# - name: VRF4 +# description: VRF4 Description +# rd: "3:4" +# +# After state: +# ------------ +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# description "VRF4 Description" +# rd "3:4" + +# Using replaced +# +# Before state: +# ------------- +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# description "VRF4 Description" +# rd "3:4" + +- name: Replace the provided configuration with the existing running configuration + arista.eos.eos_vrf_global: + config: + - name: VRF7 + description: VRF7 description + rd: "67:9" + state: replaced + +# Task Output: +# ------------ +# +# before: +# - name: VRF4 +# description: VRF4 Description +# rd: "3:4" +# +# commands: +# - vrf instance VRF7 +# - description VRF7 description +# - rd 6:9 +# +# after: +# - name: VRF4 +# description: VRF4 Description +# rd: "3:4" +# - name: VRF7 +# description: VRF7 description +# rd: "6:9" +# +# After state: +# ------------ +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# description VRF4 Description +# rd 3:4 +# ! +# vrf instance VRF7 +# description VRF7 description +# rd 6:9 +# ! +# ! + +# Using overridden +# +# Before state: +# ------------- +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# description VRF4 Description +# rd 3:4 +# ! +# vrf instance VRF7 +# description VRF7 description +# rd 6:9 +# ! +# ! + +- name: Override the provided configuration with the existing running configuration + arista.eos.eos_vrf_global: + state: overridden + config: + - name: VRF6 + description: VRF6 Description + rd: "9:8" + +# Task Output: +# ------------ +# +# before: +# - name: VRF4 +# description: VRF4 Description +# rd: "3:4" +# - name: VRF7 +# description: VRF7 description +# rd: "6:9" +# +# commands: +# - vrf instance VRF4 +# - no description VRF4 Description +# - no rd 3:4 +# - vrf instance VRF7 +# - no description VRF7 description +# - no rd 67:9 +# - vrf instance VRF6 +# - description VRF6 Description +# - rd 9:8 +# +# after: +# - name: VRF4 +# - name: VRF6 +# description: VRF6 Description +# rd: "9:8" +# - name: VRF7 +# +# After state: +# ------------- +# test#show running-config | section ^vrf +# vrf instance VRF4 +# vrf instance VRF6 +# description VRF6 Description +# rd 9:8 +# vrf instance VRF7 + +# Using deleted +# +# Before state: +# ------------- +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# vrf instance VRF6 +# description VRF6 Description +# rd 9:8 +# vrf instance VRF7 + +- name: Delete the provided configuration + arista.eos.eos_vrf_global: + config: + state: deleted + +# Task Output: +# ------------ +# +# before: +# - name: VRF4 +# - name: VRF6 +# description: VRF6 Description +# rd: "9:8" +# - name: VRF7 + +# commands: +# - vrf instance VRF4 +# - vrf instance VRF6 +# - no description VRF6 Description +# - no rd 9:8 +# - vrf instance VRF7 +# +# after: +# - name: VRF4 +# - name: VRF6 +# - name: VRF7 +# +# After state: +# ------------ +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# vrf instance VRF6 +# vrf instance VRF7 + +# Using purged +# +# Before state: +# ------------- +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# vrf instance VRF6 +# vrf instance VRF7 + +- name: Purge all the configuration from the device + arista.eos.eos_vrf_global: + state: purged + +# Task Output: +# ------------ +# +# before: +# - name: VRF4 +# - name: VRF6 +# - name: VRF7 +# +# commands: +# - no vrf instance VRF4 +# - no vrf instance VRF6 +# - no vrf instance VRF7 +# +# after: [] +# +# After state: +# ------------- +# test#show running-config | section ^vrf +# - + +# Using rendered +# +- name: Render provided configuration with device configuration + arista.eos.eos_vrf_global: + config: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + state: rendered + +# Task Output: +# ------------ +# +# rendered: +# - vrf instance VRF4 +# - description VRF4 Description +# - rd 3:4 + +# Using gathered +# +# Before state: +# ------------- +# +# test#show running-config | section ^vrf +# vrf instance VRF4 +# description "VRF4 Description" +# rd "3:4" + +- name: Gather existing running configuration + arista.eos.eos_vrf_global: + state: gathered + +# Task Output: +# ------------ +# +# gathered: +# - name: VRF4 +# description: VRF4 Description +# rd: "3:4" + +# Using parsed +# +# File: parsed.cfg +# ---------------- +# +# vrf instance test +# description "This is test VRF" +# rd "testing" +# ! +# ! +# vrf my_vrf +# description "this is sample vrf for feature testing" +# rd "2:3" +# ! +# ! + +- name: Parse the provided configuration + arista.eos.eos_vrf_global: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task Output: +# ------------ +# +# parsed: +# - description: This is test VRF +# name: test +# rd: testing +# - description: this is sample vrf for feature testing +# name: my_vrf +# rd: '2:3' +""" + +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: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration after module execution. + 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: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - vrf instance test + - description "This is test VRF" + - rd 3:4 +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 instance test + - description "This is test VRF" + - rd 3:4 +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.arista.eos.plugins.module_utils.network.eos.argspec.vrf_global.vrf_global import ( + Vrf_globalArgs, +) +from ansible_collections.arista.eos.plugins.module_utils.network.eos.config.vrf_global.vrf_global import ( + Vrf_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vrf_globalArgs.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_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/eos_vrf_global/defaults/main.yaml b/tests/integration/targets/eos_vrf_global/defaults/main.yaml new file mode 100644 index 000000000..871ea460c --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "[^_].*" diff --git a/tests/integration/targets/eos_vrf_global/meta/main.yaml b/tests/integration/targets/eos_vrf_global/meta/main.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/eos_vrf_global/tasks/cli.yaml b/tests/integration/targets/eos_vrf_global/tasks/cli.yaml new file mode 100644 index 000000000..86ca0d9cb --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/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/eos_vrf_global/tasks/main.yaml b/tests/integration/targets/eos_vrf_global/tasks/main.yaml new file mode 100644 index 000000000..c3b429408 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Invoke cli tasks + ansible.builtin.include_tasks: cli.yaml + tags: + - network_cli diff --git a/tests/integration/targets/eos_vrf_global/tests/common/_parsed.cfg b/tests/integration/targets/eos_vrf_global/tests/common/_parsed.cfg new file mode 100644 index 000000000..bd901a535 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/_parsed.cfg @@ -0,0 +1,6 @@ +vrf instance test + description "This is test VRF" + rd "testing" +vrf instance my_vrf + description "this is sample vrf for feature testing" + rd "2:3" diff --git a/tests/integration/targets/eos_vrf_global/tests/common/_populate.yaml b/tests/integration/targets/eos_vrf_global/tests/common/_populate.yaml new file mode 100644 index 000000000..9ba5f3b26 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/_populate.yaml @@ -0,0 +1,9 @@ +--- +- name: Merge the provided configuration with the existing running configuration + register: result + become: true + arista.eos.eos_config: + lines: + - vrf instance VRF4 + - description VRF4 Description + - rd 3:4 diff --git a/tests/integration/targets/eos_vrf_global/tests/common/_remove_config.yaml b/tests/integration/targets/eos_vrf_global/tests/common/_remove_config.yaml new file mode 100644 index 000000000..6017edbbc --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/_remove_config.yaml @@ -0,0 +1,12 @@ +--- +- name: Remove VRF global configurations + become: true + register: result + arista.eos.eos_config: + lines: + - no vrf instance VRF4 + - no vrf instance VRF6 + - no vrf instance VRF7 + - no vrf instance VRF1 + - no vrf instance my_vrf + - no vrf instance test diff --git a/tests/integration/targets/eos_vrf_global/tests/common/deleted.yaml b/tests/integration/targets/eos_vrf_global/tests/common/deleted.yaml new file mode 100644 index 000000000..4cd898154 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/deleted.yaml @@ -0,0 +1,46 @@ +--- +- ansible.builtin.debug: + msg: Start eos_vrf_global 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 + become: true + register: result + arista.eos.eos_vrf_global: &deleted + config: + - name: VRF4 + state: deleted + + - ansible.builtin.assert: + that: + - result.changed == true + + - 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: + - "{{ deleted['before'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dicts are correctly generated + ansible.builtin.assert: + that: + - deleted['after'] == result['after'] + + - name: Idempotency check + become: true + register: result + arista.eos.eos_vrf_global: *deleted + - ansible.builtin.assert: + that: + - result.commands|length == 0 + - result.changed == false + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/eos_vrf_global/tests/common/empty_config.yaml b/tests/integration/targets/eos_vrf_global/tests/common/empty_config.yaml new file mode 100644 index 000000000..79487d988 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/empty_config.yaml @@ -0,0 +1,66 @@ +--- +- ansible.builtin.debug: + msg: START eos_vrf_global empty_config integration tests on connection={{ ansible_connection }} + +- name: Merged with empty configuration should give appropriate error message + register: result + ignore_errors: true + arista.eos.eos_vrf_global: + config: + state: merged + become: true + +- 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 + arista.eos.eos_vrf_global: + config: + state: replaced + become: true + +- 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 + arista.eos.eos_vrf_global: + config: + state: overridden + become: true + +- 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 + arista.eos.eos_vrf_global: + config: + state: rendered + become: true + +- 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 + arista.eos.eos_vrf_global: + running_config: + state: parsed + become: true + +- ansible.builtin.assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' + +- ansible.builtin.debug: + msg: END eos_vrf_global empty_config integration tests on connection={{ ansible_connection }} diff --git a/tests/integration/targets/eos_vrf_global/tests/common/gathered.yaml b/tests/integration/targets/eos_vrf_global/tests/common/gathered.yaml new file mode 100644 index 000000000..7ec19c815 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/gathered.yaml @@ -0,0 +1,23 @@ +--- +- ansible.builtin.debug: + msg: START eos_vrf_global 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 + become: true + register: result + arista.eos.eos_vrf_global: + state: gathered + + - name: Assert + ansible.builtin.assert: + that: + - result.changed == false + - gathered['after'] == result['gathered'] + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/eos_vrf_global/tests/common/merged.yaml b/tests/integration/targets/eos_vrf_global/tests/common/merged.yaml new file mode 100644 index 000000000..275edf35e --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/merged.yaml @@ -0,0 +1,43 @@ +--- +- ansible.builtin.debug: + msg: Start eos_vrf_global merged integration tests connection={{ ansible_connection}} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- block: + - name: Merge provided configuration with device configuration + become: true + arista.eos.eos_vrf_global: &merged + config: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + state: merged + register: result + + - name: Assert that before dicts were correctly generated + ansible.builtin.assert: + that: "{{ result['before'] == [] }}" + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that after dicts were correctly generated + ansible.builtin.assert: + that: + - "{{ merged['after'] == result['after'] }}" + + - name: Merge the provided configuration with the existing running configuration (idempotent) + become: true + arista.eos.eos_vrf_global: *merged + register: result + + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + - result.commands|length == 0 + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/eos_vrf_global/tests/common/overridden.yaml b/tests/integration/targets/eos_vrf_global/tests/common/overridden.yaml new file mode 100644 index 000000000..546efdad7 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/overridden.yaml @@ -0,0 +1,51 @@ +--- +- ansible.builtin.debug: + msg: START eos_vrf_global overridden integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _populate.yaml + +- block: + - name: Override the provided configuration with the existing running configuration + become: true + arista.eos.eos_vrf_global: &overridden + config: + - name: VRF4 + - name: VRF6 + description: VRF6 Description + rd: "9:8" + 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 + become: true + arista.eos.eos_vrf_global: *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/eos_vrf_global/tests/common/parsed.yaml b/tests/integration/targets/eos_vrf_global/tests/common/parsed.yaml new file mode 100644 index 000000000..bbc72c1b4 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/parsed.yaml @@ -0,0 +1,15 @@ +--- +- ansible.builtin.debug: + msg: START eos_vrf_global parsed integration tests on connection={{ ansible_connection }} + +- name: Parse externally provided VRF configuration + become: true + register: result + arista.eos.eos_vrf_global: + 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/eos_vrf_global/tests/common/purged.yaml b/tests/integration/targets/eos_vrf_global/tests/common/purged.yaml new file mode 100644 index 000000000..16226783e --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/purged.yaml @@ -0,0 +1,43 @@ +--- +- ansible.builtin.debug: + msg: Start eos_vrf_global purged integration tests connection={{ ansible_connection}} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- ansible.builtin.include_tasks: _populate.yaml + +- block: + - name: Purge all VRF configuration from the device + become: true + arista.eos.eos_vrf_global: &id001 + state: purged + register: result + + - name: Assert that before dicts are correctly generated + ansible.builtin.assert: + that: + - merged['after'] == result['before'] + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "'no vrf instance VRF4' in result.commands" + - result.commands|length == 1 + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - result['after'] == [] + + - name: Purge all VRF configuration from the device (idempotent) + become: true + register: result + arista.eos.eos_vrf_global: *id001 + - name: Assert that task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + - result.commands|length == 0 + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/eos_vrf_global/tests/common/rendered.yaml b/tests/integration/targets/eos_vrf_global/tests/common/rendered.yaml new file mode 100644 index 000000000..f19f61c54 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/rendered.yaml @@ -0,0 +1,20 @@ +--- +- ansible.builtin.debug: + msg: START eos_vrf_global rendered integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml + +- name: Render provided configuration with device configuration + become: true + arista.eos.eos_vrf_global: + config: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + state: rendered + register: result + +- name: Assert that correct set of commands were rendered + ansible.builtin.assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['rendered']) |length == 0 }}" diff --git a/tests/integration/targets/eos_vrf_global/tests/common/replaced.yaml b/tests/integration/targets/eos_vrf_global/tests/common/replaced.yaml new file mode 100644 index 000000000..cc612d1ac --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/tests/common/replaced.yaml @@ -0,0 +1,44 @@ +--- +- ansible.builtin.debug: + msg: Start eos_vrf_global 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 + become: true + arista.eos.eos_vrf_global: &replaced + config: + - name: VRF7 + description: VRF7 description + rd: "6:9" + 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'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Idempotency check + become: true + register: result + arista.eos.eos_vrf_global: *replaced + - ansible.builtin.assert: + that: + - result['changed'] == false + - result.commands|length == 0 + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/eos_vrf_global/vars/main.yaml b/tests/integration/targets/eos_vrf_global/vars/main.yaml new file mode 100644 index 000000000..0bde1d050 --- /dev/null +++ b/tests/integration/targets/eos_vrf_global/vars/main.yaml @@ -0,0 +1,73 @@ +--- +merged: + before: [] + commands: + - vrf instance VRF4 + - description VRF4 Description + - rd 3:4 + after: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + +replaced: + before: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + commands: + - vrf instance VRF7 + - description VRF7 description + - rd 6:9 + after: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + - name: VRF7 + description: VRF7 description + rd: "6:9" + +overridden: + before: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + commands: + - vrf instance VRF4 + - no description VRF4 Description + - no rd 3:4 + - vrf instance VRF6 + - description VRF6 Description + - rd 9:8 + after: + - name: VRF4 + - name: VRF6 + description: VRF6 Description + rd: "9:8" + +deleted: + before: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + commands: + - vrf instance VRF4 + - no description VRF4 Description + - no rd 3:4 + after: + - name: VRF4 + +gathered: + after: + - name: VRF4 + description: VRF4 Description + rd: "3:4" + +parsed: + after: + - name: test + description: "This is test VRF" + rd: "testing" + - name: my_vrf + description: "this is sample vrf for feature testing" + rd: "2:3" diff --git a/tests/unit/modules/network/eos/test_eos_vrf_global.py b/tests/unit/modules/network/eos/test_eos_vrf_global.py new file mode 100644 index 000000000..1e0cc102c --- /dev/null +++ b/tests/unit/modules/network/eos/test_eos_vrf_global.py @@ -0,0 +1,350 @@ +# +# (c) 2024, Ansible by Red Hat, inc +# 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 unittest.mock import patch + +from ansible_collections.arista.eos.plugins.modules import eos_vrf_global +from ansible_collections.arista.eos.tests.unit.modules.utils import set_module_args + +from .eos_module import TestEosModule + + +class TestEosVrfGlobalModule(TestEosModule): + """Tests the eos_vrf_global module.""" + + module = eos_vrf_global + + def setUp(self): + """Setup for eos_vrf module tests.""" + super(TestEosVrfGlobalModule, 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.arista.eos.plugins.module_utils.network.eos.facts.vrf_global.vrf_global." + "Vrf_globalFacts.get_config", + ) + self.get_config = self.mock_get_config.start() + + def tearDown(self): + """Tear down for eos_vrf_global module tests.""" + super(TestEosVrfGlobalModule, self).tearDown() + self.get_resource_connection.stop() + self.get_config.stop() + + def test_eos_vrf_global_merged_idempotent(self): + """Test the idempotent nature of the eos_vrf_global module in merged state.""" + run_cfg = dedent( + """\ + vrf instance test + description "this is sample vrf for feature testing" + rd "2:3" + """, + ) + self.get_config.return_value = run_cfg + set_module_args( + dict( + config=[ + dict( + name="test", + description="this is sample vrf for feature testing", + rd="2:3", + ), + ], + state="merged", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_eos_vrf_global_merged(self): + """Test the merged state of the eos_vrf_global module.""" + set_module_args( + dict( + config=[ + dict( + name="VRF4", + description="VRF4 Description", + rd="3:4", + ), + ], + state="merged", + ), + ) + commands = [ + "vrf instance VRF4", + "description VRF4 Description", + "rd 3:4", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_eos_vrf_global_replaced(self): + """Test the replaced state of the eos_vrf_global module.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + description VRF4 description + rd 3:4 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF4", + description="VRF4 description", + rd="3:4", + ), + dict( + name="VRF7", + description="VRF7 description", + rd="67:9", + ), + ], + state="replaced", + ), + ) + commands = [ + "vrf instance VRF7", + "description VRF7 description", + "rd 67:9", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_eos_vrf_global_replaced_idempotent(self): + """Test the idempotent nature of the eos_vrf_global module in replaced state.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + description VRF4 description + rd 3:4 + vrf instance VRF7 + description VRF7 description + rd 67:9 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF4", + description="VRF4 description", + rd="3:4", + ), + dict( + name="VRF7", + description="VRF7 description", + rd="67:9", + ), + ], + state="replaced", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_eos_vrf_global_overridden(self): + """Test the overridden state of the eos_vrf_global module.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + description VRF4 description + rd 3:4 + vrf instance VRF7 + description VRF7 description + rd 6:9 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF4", + ), + dict( + name="VRF6", + description="VRF6 description", + rd="9:8", + ), + dict( + name="VRF7", + ), + ], + state="overridden", + ), + ) + commands = [ + "vrf instance VRF4", + "no description VRF4 description", + "no rd 3:4", + "vrf instance VRF7", + "no description VRF7 description", + "no rd 6:9", + "vrf instance VRF6", + "description VRF6 description", + "rd 9:8", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_eos_vrf_global_overridden_idempotent(self): + """Test the idempotent nature of the eos_vrf_global module in overridden state.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + vrf instance VRF6 + description VRF6 description + rd 67:9 + vrf instance VRF7 + """, + ) + self.get_config.return_value = run_cfg + + set_module_args( + dict( + config=[ + dict( + name="VRF4", + ), + dict( + name="VRF6", + description="VRF6 description", + rd="67:9", + ), + dict( + name="VRF7", + ), + ], + state="overridden", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_eos_vrf_global_deleted(self): + """Test the deleted state of the eos_vrf_global module.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + vrf instance VRF6 + description VRF6 description + rd 9:8 + vrf instance VRF7 + """, + ) + self.get_config.return_value = run_cfg + set_module_args( + dict( + config=[ + dict( + name="VRF4", + ), + dict( + name="VRF6", + ), + dict( + name="VRF7", + ), + ], + state="deleted", + ), + ) + + commands = [ + "vrf instance VRF6", + "no description VRF6 description", + "no rd 9:8", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_eos_vrf_global_deleted_idempotent(self): + """Test the idempotent nature of the eos_vrf_global 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_eos_vrf_global_purged(self): + """Test the purged state of the eos_vrf_global module.""" + run_cfg = dedent( + """\ + vrf instance VRF4 + vrf instance VRF6 + vrf instance VRF7 + """, + ) + self.get_config.return_value = run_cfg + set_module_args(dict(state="purged")) + commands = [ + "no vrf instance VRF4", + "no vrf instance VRF6", + "no vrf instance VRF7", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands)) + + def test_eos_vrf_global_rendered(self): + """Test the rendered state of the eos_vrf_global module.""" + set_module_args( + dict( + config=[ + dict( + name="VRF4", + description="VRF4 Description", + rd="3:4", + ), + ], + state="rendered", + ), + ) + commands = [ + "vrf instance VRF4", + "description VRF4 Description", + "rd 3:4", + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result["rendered"]), sorted(commands)) + + def test_eos_vrf_global_parsed(self): + """Test the parsed state of the eos_vrf_global module.""" + run_cfg = dedent( + """\ + vrf instance my_vrf + description "this is sample vrf for feature testing" + rd "2:3" + """, + ) + set_module_args(dict(running_config=run_cfg, state="parsed")) + result = self.execute_module(changed=False) + parsed_list = [ + { + "name": "my_vrf", + "description": "this is sample vrf for feature testing", + "rd": "2:3", + }, + ] + + self.assertEqual(parsed_list, result["parsed"])