From 726d6455d813e40f37e1298f0dce8a2f76709de4 Mon Sep 17 00:00:00 2001 From: GomathiselviS Date: Tue, 25 Feb 2020 07:50:28 -0500 Subject: [PATCH] eos static_routes module added (#65480) * Adding files for RM static_routes * Added Integration tests * Added Unit testcases * Addressed review comments * corrected lint errors * corrected documentation errors * Lint errors * corrected test/sanity * corrected documentation for deprecation * corrected case sensitivity * Again Documentation eroor * Lint errors again * corrected deprecated module in ignoretxt * added new gethered,rendered,parsed state checks to unit test * New code broke the old flow-fixed * Lint errs * Added check for running_config * Add rtt testcase * Fixed unit tcs * lint errors * lint errors * Modified replaced operation behavior * updated documentation and tests for delete opration * fixed shippable errors * review comments and flake8 error fix * syntax errors fixed --- .../eos/argspec/static_routes/__init__.py | 0 .../argspec/static_routes/static_routes.py | 115 ++ .../eos/config/static_routes/__init__.py | 0 .../eos/config/static_routes/static_routes.py | 390 +++++ .../module_utils/network/eos/facts/facts.py | 2 + .../eos/facts/static_routes/__init__.py | 0 .../eos/facts/static_routes/static_routes.py | 202 +++ ...s_static_route.py => _eos_static_route.py} | 6 +- .../modules/network/eos/eos_static_routes.py | 1444 +++++++++++++++++ .../eos_static_routes/defaults/main.yaml | 3 + .../targets/eos_static_routes/meta/main.yaml | 2 + .../targets/eos_static_routes/tasks/cli.yaml | 17 + .../targets/eos_static_routes/tasks/eapi.yaml | 16 + .../targets/eos_static_routes/tasks/main.yaml | 3 + .../eos_static_routes/tests/cli/_parsed.cfg | 7 + .../tests/cli/_populate.yaml | 16 + .../tests/cli/_remove_config.yaml | 20 + .../eos_static_routes/tests/cli/deleted.yaml | 224 +++ .../eos_static_routes/tests/cli/gathered.yaml | 75 + .../eos_static_routes/tests/cli/merged.yaml | 102 ++ .../tests/cli/overridden.yaml | 61 + .../eos_static_routes/tests/cli/parsed.yaml | 37 + .../eos_static_routes/tests/cli/rendered.yaml | 53 + .../eos_static_routes/tests/cli/replaced.yaml | 90 + .../eos_static_routes/tests/cli/rtt.yaml | 177 ++ test/sanity/ignore.txt | 14 +- .../eos/fixtures/eos_static_routes_config.cfg | 3 + .../fixtures/eos_static_routes_config1.cfg | 1 + .../network/eos/test_eos_static_routes.py | 308 ++++ 29 files changed, 3380 insertions(+), 8 deletions(-) create mode 100644 lib/ansible/module_utils/network/eos/argspec/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/argspec/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/eos/config/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/config/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/eos/facts/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/facts/static_routes/static_routes.py rename lib/ansible/modules/network/eos/{eos_static_route.py => _eos_static_route.py} (98%) create mode 100644 lib/ansible/modules/network/eos/eos_static_routes.py create mode 100644 test/integration/targets/eos_static_routes/defaults/main.yaml create mode 100644 test/integration/targets/eos_static_routes/meta/main.yaml create mode 100644 test/integration/targets/eos_static_routes/tasks/cli.yaml create mode 100644 test/integration/targets/eos_static_routes/tasks/eapi.yaml create mode 100644 test/integration/targets/eos_static_routes/tasks/main.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/_parsed.cfg create mode 100644 test/integration/targets/eos_static_routes/tests/cli/_populate.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/_remove_config.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/deleted.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/gathered.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/merged.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/overridden.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/parsed.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/rendered.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/replaced.yaml create mode 100644 test/integration/targets/eos_static_routes/tests/cli/rtt.yaml create mode 100644 test/units/modules/network/eos/fixtures/eos_static_routes_config.cfg create mode 100644 test/units/modules/network/eos/fixtures/eos_static_routes_config1.cfg create mode 100644 test/units/modules/network/eos/test_eos_static_routes.py diff --git a/lib/ansible/module_utils/network/eos/argspec/static_routes/__init__.py b/lib/ansible/module_utils/network/eos/argspec/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/argspec/static_routes/static_routes.py b/lib/ansible/module_utils/network/eos/argspec/static_routes/static_routes.py new file mode 100644 index 00000000000..f4b9277b1e2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/static_routes/static_routes.py @@ -0,0 +1,115 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The arg spec for the eos_static_routes module +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class Static_routesArgs(object): + """The arg spec for the eos_static_routes module + """ + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'address_families': { + 'elements': 'dict', + 'options': { + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + }, + 'routes': { + 'elements': 'dict', + 'options': { + 'dest': { + 'required': True, + 'type': 'str' + }, + 'next_hops': { + 'elements': 'dict', + 'options': { + 'admin_distance': { + 'type': 'int' + }, + 'description': { + 'type': 'str' + }, + 'forward_router_address': { + 'type': 'str' + }, + 'interface': { + 'type': 'str' + }, + 'nexthop_grp': { + 'type': 'str' + }, + 'mpls_label': { + 'type': 'int' + }, + 'tag': { + 'type': 'int' + }, + 'track': { + 'type': 'str' + }, + 'vrf': { + 'type': 'str' + } + }, + 'type': 'list' + } + }, + 'type': 'list' + } + }, + 'type': 'list' + }, + 'vrf': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'running_config': { + 'type': 'str' + }, + 'state': { + 'choices': [ + 'deleted', 'merged', 'overridden', 'replaced', 'gathered', + 'rendered', 'parsed' + ], + 'default': + 'merged', + 'type': + 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/eos/config/static_routes/__init__.py b/lib/ansible/module_utils/network/eos/config/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/config/static_routes/static_routes.py b/lib/ansible/module_utils/network/eos/config/static_routes/static_routes.py new file mode 100644 index 00000000000..ae3e4609c16 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/config/static_routes/static_routes.py @@ -0,0 +1,390 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The eos_static_routes class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import remove_empties +from ansible.module_utils.network.eos.facts.facts import Facts + + +class Static_routes(ConfigBase): + """ + The eos_static_routes class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'static_routes', + ] + + def __init__(self, module): + super(Static_routes, self).__init__(module) + + def get_static_routes_facts(self, data=None): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources, data=data) + static_routes_facts = facts['ansible_network_resources'].get('static_routes') + if not static_routes_facts: + return [] + return static_routes_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + if self.state in self.ACTION_STATES: + existing_static_routes_facts = self.get_static_routes_facts() + else: + existing_static_routes_facts = [] + + if self.state in self.ACTION_STATES or self.state == 'rendered': + commands.extend(self.set_config(existing_static_routes_facts)) + + if commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + for command in commands: + self._connection.edit_config(command) + result['changed'] = True + if self.state in self.ACTION_STATES: + result['commands'] = commands + if self.state in self.ACTION_STATES or self.state == 'gathered': + changed_static_routes_facts = self.get_static_routes_facts() + elif self.state == 'rendered': + result['rendered'] = commands + elif self.state == 'parsed': + if not self._module.params['running_config']: + self._module.fail_json(msg="Value of running_config parameter must not be empty for state parsed") + result['parsed'] = self.get_static_routes_facts(data=self._module.params['running_config']) + else: + changed_static_routes_facts = [] + + if self.state in self.ACTION_STATES: + result['before'] = existing_static_routes_facts + if result['changed']: + result['after'] = changed_static_routes_facts + elif self.state == 'gathered': + result['gathered'] = changed_static_routes_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_static_routes_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + onbox_configs = [] + for h in existing_static_routes_facts: + return_command = add_commands(h) + for command in return_command: + onbox_configs.append(command) + config = self._module.params.get('config') + want = [] + if config: + for w in config: + want.append(remove_empties(w)) + have = existing_static_routes_facts + resp = self.set_state(want, have) + for want_config in resp: + if want_config not in onbox_configs: + commands.append(want_config) + return commands + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if self.state in ('merged', 'replaced', 'overridden') and not want: + self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(self.state)) + state = self._module.params['state'] + if state == 'overridden': + commands = self._state_overridden(want, have) + elif state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged' or self.state == 'rendered': + commands = self._state_merged(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + return commands + + @staticmethod + def _state_replaced(want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + haveconfigs = [] + vrf = get_vrf(want) + dest = get_dest(want) + for h in have: + return_command = add_commands(h) + for command in return_command: + for d in dest: + if d in command: + if vrf is None: + if "vrf" not in command: + haveconfigs.append(command) + else: + if vrf in command: + haveconfigs.append(command) + wantconfigs = set_commands(want, have) + + removeconfigs = list(set(haveconfigs) - set(wantconfigs)) + for command in removeconfigs: + commands.append("no " + command) + for wantcmd in wantconfigs: + commands.append(wantcmd) + return commands + + @staticmethod + def _state_overridden(want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + haveconfigs = [] + for h in have: + return_command = add_commands(h) + for command in return_command: + haveconfigs.append(command) + wantconfigs = set_commands(want, have) + idempotentconfigs = list(set(haveconfigs) - set(wantconfigs)) + if not idempotentconfigs: + return idempotentconfigs + removeconfigs = list(set(haveconfigs) - set(wantconfigs)) + for command in removeconfigs: + commands.append("no " + command) + for wantcmd in wantconfigs: + commands.append(wantcmd) + return commands + + @staticmethod + def _state_merged(want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + return set_commands(want, have) + + @staticmethod + def _state_deleted(want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + if not want: + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + commands.append(command) + else: + for w in want: + return_command = del_commands(w, have) + for command in return_command: + commands.append(command) + return commands + + +def set_commands(want, have): + commands = [] + for w in want: + return_command = add_commands(w) + for command in return_command: + commands.append(command) + return commands + + +def add_commands(want): + commandset = [] + if not want: + return commandset + vrf = want["vrf"] if "vrf" in want.keys() and want["vrf"] is not None else None + for address_family in want["address_families"]: + for route in address_family["routes"]: + for next_hop in route["next_hops"]: + commands = [] + if address_family["afi"] == "ipv4": + commands.append('ip route') + else: + commands.append('ipv6 route') + if vrf: + commands.append(' vrf ' + vrf) + if not re.search(r'/', route["dest"]): + mask = route["dest"].split()[1] + cidr = get_net_size(mask) + commands.append(' ' + route["dest"].split()[0] + '/' + cidr) + else: + commands.append(' ' + route["dest"]) + if "interface" in next_hop.keys(): + commands.append(' ' + next_hop["interface"]) + if "nexthop_grp" in next_hop.keys(): + commands.append(' Nexthop-Group' + ' ' + next_hop["nexthop_grp"]) + if "forward_router_address" in next_hop.keys(): + commands.append(' ' + next_hop["forward_router_address"]) + if "mpls_label" in next_hop.keys(): + commands.append(' label ' + str(next_hop["mpls_label"])) + if "track" in next_hop.keys(): + commands.append(' track ' + next_hop["track"]) + if "admin_distance" in next_hop.keys(): + commands.append(' ' + str(next_hop["admin_distance"])) + if "description" in next_hop.keys(): + commands.append(' name ' + str(next_hop["description"])) + if "tag" in next_hop.keys(): + commands.append(' tag ' + str(next_hop["tag"])) + + config_commands = "".join(commands) + commandset.append(config_commands) + return commandset + + +def del_commands(want, have): + commandset = [] + haveconfigs = [] + for h in have: + return_command = add_commands(h) + for command in return_command: + command = "no " + command + haveconfigs.append(command) + if want is None or "address_families" not in want.keys(): + commandset = haveconfigs + if "address_families" not in want.keys() and "vrf" in want.keys(): + commandset = [] + for command in haveconfigs: + if want["vrf"] in command: + commandset.append(command) + elif want is not None and "vrf" not in want.keys() and "address_families" not in want.keys(): + commandset = [] + for command in haveconfigs: + if "vrf" not in command: + commandset.append(command) + + elif want["address_families"]: + vrf = want["vrf"] if "vrf" in want.keys() and want["vrf"] else None + for address_family in want["address_families"]: + if "routes" not in address_family.keys(): + for command in haveconfigs: + afi = "ip " if address_family["afi"] == "ipv4" else "ipv6" + if afi in command: + if vrf: + if vrf in command: + commandset.append(command) + else: + commandset.append(command) + else: + for route in address_family["routes"]: + if not re.search(r'/', route["dest"]): + mask = route["dest"].split()[1] + cidr = get_net_size(mask) + destination = route["dest"].split()[0] + '/' + cidr + else: + destination = route["dest"] + if "next_hops" not in route.keys(): + for command in haveconfigs: + if destination in command: + if vrf: + if vrf in command: + commandset.append(command) + else: + commandset.append(command) + else: + for next_hop in route["next_hops"]: + commands = [] + if address_family["afi"] == "ipv4": + commands.append('no ip route') + else: + commands.append('no ipv6 route') + if vrf: + commands.append(' vrf ' + vrf) + commands.append(' ' + destination) + if "interface" in next_hop.keys(): + commands.append(' ' + next_hop["interface"]) + if "nexhop_grp" in next_hop.keys(): + commands.append(' Nexthop-Group' + ' ' + next_hop["nexthop_grp"]) + if "forward_router_address" in next_hop.keys(): + commands.append(' ' + next_hop["forward_router_address"]) + if "mpls_label" in next_hop.keys(): + commands.append(' label ' + str(next_hop["mpls_label"])) + if "track" in next_hop.keys(): + commands.append(' track ' + next_hop["track"]) + if "admin_distance" in next_hop.keys(): + commands.append(' ' + str(next_hop["admin_distance"])) + if "description" in next_hop.keys(): + commands.append(' name ' + str(next_hop["description"])) + if "tag" in next_hop.keys(): + commands.append(' tag ' + str(next_hop["tag"])) + + config_commands = "".join(commands) + commandset.append(config_commands) + return commandset + + +def get_net_size(netmask): + binary_str = '' + netmask = netmask.split('.') + for octet in netmask: + binary_str += bin(int(octet))[2:].zfill(8) + return str(len(binary_str.rstrip('0'))) + + +def get_vrf(config): + vrf = "" + for c in config: + vrf = c["vrf"] if "vrf" in c.keys() and c["vrf"] else None + return vrf + + +def get_dest(config): + dest = [] + for c in config: + for address_family in c["address_families"]: + for route in address_family["routes"]: + dest.append(route['dest']) + return dest diff --git a/lib/ansible/module_utils/network/eos/facts/facts.py b/lib/ansible/module_utils/network/eos/facts/facts.py index c82997a1321..4ff7d871502 100644 --- a/lib/ansible/module_utils/network/eos/facts/facts.py +++ b/lib/ansible/module_utils/network/eos/facts/facts.py @@ -23,6 +23,7 @@ from ansible.module_utils.network.eos.facts.vlans.vlans import VlansFacts from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces from ansible.module_utils.network.eos.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts from ansible.module_utils.network.eos.facts.acls.acls import AclsFacts +from ansible.module_utils.network.eos.facts.static_routes.static_routes import Static_routesFacts FACT_LEGACY_SUBSETS = dict( @@ -43,6 +44,7 @@ FACT_RESOURCE_SUBSETS = dict( vlans=VlansFacts, acl_interfaces=Acl_interfacesFacts, acls=AclsFacts, + static_routes=Static_routesFacts, ) diff --git a/lib/ansible/module_utils/network/eos/facts/static_routes/__init__.py b/lib/ansible/module_utils/network/eos/facts/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/eos/facts/static_routes/static_routes.py b/lib/ansible/module_utils/network/eos/facts/static_routes/static_routes.py new file mode 100644 index 00000000000..810b2bfa0ec --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/static_routes/static_routes.py @@ -0,0 +1,202 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The eos static_routes fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +from copy import deepcopy + +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.eos.argspec.static_routes.static_routes import Static_routesArgs + + +class Static_routesFacts(object): + """ The eos static_routes fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Static_routesArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_device_data(self, connection): + return connection.get('show running-config | grep route') + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for static_routes + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_device_data(connection) + + # split the config into instances of the resource + resource_delim = 'ip.* route' + find_pattern = r'(?:^|\n)%s.*?(?=(?:^|\n)%s|$)' % (resource_delim, + resource_delim) + resources = [p.strip() for p in re.findall(find_pattern, data)] + resources_without_vrf = [] + resource_vrf = {} + for resource in resources: + if resource and "vrf" not in resource: + resources_without_vrf.append(resource) + else: + vrf = re.search(r'ip(v6)* route vrf (.*?) .*', resource) + if vrf.group(2) in resource_vrf.keys(): + vrf_val = resource_vrf[vrf.group(2)] + vrf_val.append(resource) + resource_vrf.update({vrf.group(2): vrf_val}) + else: + resource_vrf.update({vrf.group(2): [resource]}) + resources_without_vrf = ["\n".join(resources_without_vrf)] + for vrf in resource_vrf.keys(): + vrflist = ["\n".join(resource_vrf[vrf])] + resource_vrf.update({vrf: vrflist}) + objs = [] + for resource in resources_without_vrf: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + for resource in resource_vrf.keys(): + if resource: + obj = self.render_config(self.generated_spec, resource_vrf[resource][0]) + if obj: + objs.append(obj) + ansible_facts['ansible_network_resources'].pop('static_routes', None) + facts = {} + if objs: + facts['static_routes'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['static_routes'].append(utils.remove_empties(cfg)) + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + address_family_dict = {} + route_dict = {} + dest_list = [] + afi_list = [] + vrf_list = [] + routes = [] + config["address_families"] = [] + next_hops = {} + interface_list = ["Ethernet", "Loopback", "Management", + "Port-Channel", "Tunnel", "Vlan", "Vxlan", "vtep"] + conf_list = conf.split('\n') + for conf_elem in conf_list: + matches = re.findall(r'(ip|ipv6) route ([\d\.\/:]+|vrf) (.+)$', conf_elem) + if matches: + remainder = matches[0][2].split() + route_update = False + if matches[0][1] == "vrf": + vrf = remainder.pop(0) + # new vrf + if vrf not in vrf_list and vrf_list: + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + route_update = True + config.update({"vrf": vrf}) + vrf_list.append(vrf) + dest = remainder.pop(0) + else: + config["vrf"] = None + dest = matches[0][1] + afi = "ipv4" if matches[0][0] == "ip" else "ipv6" + if afi not in afi_list: + if afi_list and not route_update: + # new afi and not the first updating all prev configs + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + route_update = True + address_family_dict = {} + address_family_dict.update({"afi": afi}) + routes = [] + afi_list.append(afi) + # To check the format of the dest + prefix = re.search(r'/', dest) + if not prefix: + dest = dest + ' ' + remainder.pop(0) + if dest not in dest_list: + # For new dest and not the first dest + if dest_list and not route_update: + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + dest_list.append(dest) + next_hops = [] + route_dict = {} + route_dict.update({"dest": dest}) + nexthops = {} + nxthop_addr = re.search(r'[\.\:]', remainder[0]) + if nxthop_addr: + nexthops.update({"interface": remainder.pop(0)}) + if remainder and remainder[0] == "label": + nexthops.update({"mpls_label": remainder.pop(1)}) + remainder.pop(0) + elif re.search(r'Nexthop-Group', remainder[0]): + nexthops.update({"nexthop_grp": remainder.pop(1)}) + remainder.pop(0) + else: + interface = remainder.pop(0) + if interface in interface_list: + interface = interface + " " + remainder.pop(0) + nexthops.update({"interface": interface}) + for attribute in remainder: + forward_addr = re.search(r'([\dA-Fa-f]+[:\.]+)+[\dA-Fa-f]+', attribute) + if forward_addr: + nexthops.update({"forward_router_address": remainder.pop(remainder.index(attribute))}) + for attribute in remainder: + for params in ["tag", "name", "track"]: + if attribute == params: + keyname = params + if attribute == "name": + keyname = "description" + nexthops.update({keyname: remainder.pop(remainder.index(attribute) + 1)}) + remainder.pop(remainder.index(attribute)) + if remainder: + metric = re.search(r'\d+', remainder[0]) + if metric: + nexthops.update({"admin_distance": remainder.pop(0)}) + next_hops.append(nexthops) + route_dict.update({"next_hops": next_hops}) + routes.append(route_dict) + address_family_dict.update({"routes": routes}) + config["address_families"].append(address_family_dict) + return utils.remove_empties(config) diff --git a/lib/ansible/modules/network/eos/eos_static_route.py b/lib/ansible/modules/network/eos/_eos_static_route.py similarity index 98% rename from lib/ansible/modules/network/eos/eos_static_route.py rename to lib/ansible/modules/network/eos/_eos_static_route.py index a0525a5ca59..ad244e4b356 100644 --- a/lib/ansible/modules/network/eos/eos_static_route.py +++ b/lib/ansible/modules/network/eos/_eos_static_route.py @@ -9,7 +9,7 @@ __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], + 'status': ['deprecated'], 'supported_by': 'network'} DOCUMENTATION = """ @@ -21,6 +21,10 @@ short_description: Manage static IP routes on Arista EOS network devices description: - This module provides declarative management of static IP routes on Arista EOS network devices. +deprecated: + removed_in: '2.13' + alternative: eos_static_routes + why: Updated modules with more functionality notes: - Tested against EOS 4.15 options: diff --git a/lib/ansible/modules/network/eos/eos_static_routes.py b/lib/ansible/modules/network/eos/eos_static_routes.py new file mode 100644 index 00000000000..c04491beb8d --- /dev/null +++ b/lib/ansible/modules/network/eos/eos_static_routes.py @@ -0,0 +1,1444 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for eos_static_routes +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +DOCUMENTATION = """ +--- +module: eos_static_routes +version_added: "2.10" +short_description: Configures and manages attributes of static routes on Arista EOS platforms. +description: This module configures and manages the attributes of static routes on Arista EOS platforms. +author: Gomathi Selvi Srinivasan (@GomathiselviS) +notes: +- Tested against Arista EOS 4.20.10M +- This module works with connection C(network_cli). See the + L(EOS Platform Options,../network/user_guide/platform_eos.html). +options: + config: + description: + - A list of configurations for static routes. + type: list + elements: dict + suboptions: + vrf: + description: + - The VRF to which the static route(s) belong. + type: str + address_families: + description: A dictionary specifying the address family to which the static route(s) belong. + type: list + elements: dict + suboptions: + afi: + description: + - Specifies the top level address family indicator. + type: str + choices: ['ipv4', 'ipv6'] + required: True + routes: + description: A dictionary that specifies the static route configurations. + elements: dict + type: list + suboptions: + dest: + description: + - Destination IPv4 subnet (CIDR or address-mask notation). + - The address format is / or . + - The mask is number in range 0-32 for IPv4 and in range 0-128 for IPv6. + type: str + required: True + next_hops: + description: + - Details of route to be taken. + type: list + elements: dict + suboptions: + forward_router_address: + description: + - Forwarding router's address on destination interface. + type: str + interface: + description: + - Outgoing interface to take. For anything except 'null0', then next hop IP address should also be configured. + - IP address of the next hop router or + - null0 Null0 interface or + - ethernet e_num Ethernet interface or + - loopback l_num Loopback interface or + - management m_num Management interface or + - port-channel p_num + - vlan v_num + - vxlan vx_num + - Nexthop-Group Specify nexthop group name + - Tunnel Tunnel interface + - vtep Configure VXLAN Tunnel End Points + type: str + nexthop_grp: + description: + - Nexthop group + type: str + admin_distance: + description: + - Preference or administrative distance of route (range 1-255). + type: int + description: + description: + - Name of the static route. + type: str + tag: + description: + - Route tag value (ranges from 0 to 4294967295). + type: int + track: + description: + - Track value (range 1 - 512). Track must already be configured on the device before adding the route. + type: str + mpls_label: + description: + - MPLS label + type: int + vrf: + description: + - VRF of the destination. + type: str + running_config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(running_config) argument allows the + implementer to pass in the configuration to use as the base + config for comparison. This value of this option should be the + output received from device by executing command + version_added: "2.10" + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + ['deleted', 'merged', 'overridden', 'replaced', 'gathered', 'rendered', 'parsed'] + default: + merged +""" +EXAMPLES = """ +# Using deleted + +# Various scenarios for delete operations: + +# Before state: +# ------------ +# veos(config)#show running-config | grep route +# ip route 10.2.2.0/24 Ethernet1 +# ip route 10.2.2.0/24 64.1.1.1 label 17 33 +# ip route 33.33.33.0/24 Nexthop-Group testgrp +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + +- name: Delete nexthop + eos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.2.2.0/24 + next_hops: + - interface: 64.1.1.1 + mpls_label: 17 + admin_distance: 33 + - dest: "33.33.33.0 255.255.255.0" + next_hops: + - interface: 'Nexthop-Group testgrp' + state: "deleted" + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 33, +# "interface": "64.1.1.1", +# "mpls_label": 17 +# } +# ] +# }, +# { +# "dest": "33.33.33.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgrp" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "changed": true, +# "commands": [ +# "no ip route 10.2.2.0/24 64.1.1.1 label 17 33", +# "no ip route 33.33.33.0/24 Nexthop-Group testgrp" +# ] + + +# After State +# ----------- + +# veos(config)#show running-config | grep route +# ip route 10.2.2.0/24 Ethernet1 +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + + +# Before State +# ____________ + +# veos(config)#show running-config | grep route +# ip route 10.2.2.0/24 Ethernet1 +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + +- name: Delete route + eos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.2.2.0/24 + state: "deleted" + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "changed": true, +# "commands": [ +# "no ip route 10.2.2.0/24 Ethernet1" +# ] + +# After State +# ----------- +# veos(config)#show running-config | grep route +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + + +# Before State: +# ------------ + +# veos(config)#show running-config | grep route +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 +# veos(config)# + +- name: Delete afi + eos_static_routes: + config: + - vrf: "testvrf" + address_families: + - afi: "ipv4" + state: "deleted" + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "changed": true, +# "commands": [ +# "no ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute" +# ], + +# After State +# ___________ + +# veos(config)#show running-config | grep route +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 + +# Before State +#------------- + +# veos(config)#show running-config | grep route +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 + +- name: Delete vrf + eos_static_routes: + config: + - vrf: testvrf + state: "deleted" + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# } +# ], +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ], +# "changed": true, +# "commands": [ +# "no ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1", +# "no ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55", +# "no ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1" +# ] +# +# After State: +# ----------- + +# veos(config)#show running-config | grep route +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# veos(config)# + + +# +# Using merged + +# Before : [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] +# +# Before State +# ------------- +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + +- name: Merge new static route configuration + eos_static_routes: + config: + - vrf: testvrf + address_families: + - afi: ipv6 + routes: + - dest: 2211::0/64 + next_hop: + - forward_router_address: 100:1::2 + interface: "Ethernet1" + state: merged + +# After State +# ----------- + +#After [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2211::0/64", +# "next_hops": [ +# { +# "aforward_router_address": 100:1::2 +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } + +# ], +# "vrf": "testvrf" +# } +# ] +# +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 2211::/64 Ethernet1 100:1::2 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + + +# Using overridden + + +# Before State +# ------------- + +# "before": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "165.10.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 100, +# "interface": "Ethernet1" +# } +# ] +# }, +# { +# "dest": "172.17.252.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgroup" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5001::/64", +# "next_hops": [ +# { +# "admin_distance": 50, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "130.1.122.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1", +# "tag": 50 +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 100 +# ip route 172.17.252.0/24 Nexthop-Group testgroup +# ip route vrf testvrf 130.1.122.0/24 Ethernet1 tag 50 +# ipv6 route 5001::/64 Ethernet1 50 +# veos(config)# + +- name: Overridden static route configuration + eos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.2.2.0/24 + next_hop: + - interface: "Ethernet1" + state: replaced + +# After State +# ----------- + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ] +# } +# ] +# veos(config)#show running-config | grep "route" +# ip route 10.2.2.0/24 Ethernet1 +# veos(config)# + + +# Using replaced + +# Before State +# ------------- + +# ip route 10.2.2.0/24 Ethernet1 +# ip route 10.2.2.0/24 64.1.1.1 label 17 33 +# ip route 33.33.33.0/24 Nexthop-Group testgrp +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 +# ipv6 route vrf testvrf 2222:6::/64 Null0 90 name testroute1 + +# [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 33, +# "interface": "64.1.1.1", +# "mpls_label": 17 +# } +# ] +# }, +# { +# "dest": "33.33.33.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgrp" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# }, +# { +# "admin_distance": 90, +# "description": "testroute1", +# "interface": "Null0" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] + +- name: Replace nexthop + eos_static_routes: + config: + - vrf: testvrf + address_families: + - afi: ipv6 + routes: + - dest: 2222:6::/64 + next_hops: + - admin_distance: 55 + interface: "Ethernet1" + state: "replaced" + +# After State +# ----------- + +# veos(config)#show running-config | grep route +# ip route 10.2.2.0/24 Ethernet1 +# ip route 10.2.2.0/24 64.1.1.1 label 17 33 +# ip route 33.33.33.0/24 Nexthop-Group testgrp +# ip route vrf testvrf 22.65.1.0/24 Null0 90 name testroute +# ipv6 route 5222:5::/64 Management1 4312:100::1 +# ipv6 route vrf testvrf 2222:6::/64 Ethernet1 55 + +# "after": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "10.2.2.0/24", +# "next_hops": [ +# { +# "interface": "Ethernet1" +# }, +# { +# "admin_distance": 33, +# "interface": "64.1.1.1", +# "mpls_label": 17 +# } +# ] +# }, +# { +# "dest": "33.33.33.0/24", +# "next_hops": [ +# { +# "nexthop_grp": "testgrp" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "5222:5::/64", +# "next_hops": [ +# { +# "forward_router_address": "4312:100::1", +# "interface": "Management1" +# } +# ] +# } +# ] +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "22.65.1.0/24", +# "next_hops": [ +# { +# "admin_distance": 90, +# "description": "testroute", +# "interface": "Null0" +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2222:6::/64", +# "next_hops": [ +# { +# "admin_distance": 55, +# "interface": "Ethernet1" +# } +# ] +# } +# ] +# } +# ], +# "vrf": "testvrf" +# } +# ] + +# Before State +# ------------- +# veos(config)#show running-config | grep "route" +# ip route 165.10.1.0/24 Ethernet1 10.1.1.2 100 +# ipv6 route 5001::/64 Ethernet1 +# veos(config)# + + +- name: Gather the exisitng condiguration + eos_static_routes: + state: gathered + +# returns : +# eos_static_routes: +# config: +# - address_families: +# - afi: ipv4 +# routes: +# - dest: 165.10.1.0/24 +# next_hop: +# - forward_router_address: 10.1.1.2 +# interface: "Ethernet1" +# admin_distance: 100 +# - afi: ipv6 +# routes: +# - dest: 5001::/64 +# next_hop: +# - interface: "Ethernet1" + + +# Using rendered + +# eos_static_routes: +# config: +# - address_families: +# - afi: ipv4 +# routes: +# - dest: 165.10.1.0/24 +# next_hop: +# - forward_router_address: 10.1.1.2 +# interface: "Ethernet1" +# admin_distance: 100 +# - afi: ipv6 +# routes: +# - dest: 5001::/64 +# next_hop: +# - interface: "Ethernet1" + +# returns: + +# ip route 165.10.1.0/24 Ethernet1 10.1.1.2 100 +# ipv6 route 5001::/64 Ethernet1 + + +""" +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: + - ip route vrf vrf1 192.2.2.0/24 125.2.3.1 93 +rendered: + description: The set of CLI commands generated from the value in C(config) option + returned: When C(state) is I(rendered) + type: list + sample: > + "address_families": [ + { + "afi": "ipv4", + "routes": [ + { + "dest": "192.2.2.0/24", + "next_hops": [ + { + "admin_distance": 93, + "description": null, + "forward_router_address": null, + "interface": "125.2.3.1", + "mpls_label": null, + "nexthop_grp": null, + "tag": null, + "track": null, + "vrf": null + } + ] + } + ] + } + ], + "vrf": "vrf1" + } + ], + "running_config": null, + "state": "rendered" + } +gathered: + description: The configuration as structured data transformed for the running configuration + fetched from remote host + returned: When C(state) is I(gathered) + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +parsed: + description: The configuration as structured data transformed for the value of + C(running_config) option + returned: When C(state) is I(parsed) + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.eos.argspec.static_routes.static_routes import Static_routesArgs +from ansible.module_utils.network.eos.config.static_routes.static_routes import Static_routes + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + + required_if = [('state', 'merged', ('config',)), + ('state', 'replaced', ('config',)), + ('state', 'overridden', ('config',)), + ('state', 'parsed', ('running_config',))] + mutually_exclusive = [('config', 'running_config')] + + module = AnsibleModule(argument_spec=Static_routesArgs.argument_spec, + required_if=required_if, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/eos_static_routes/defaults/main.yaml b/test/integration/targets/eos_static_routes/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/eos_static_routes/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/eos_static_routes/meta/main.yaml b/test/integration/targets/eos_static_routes/meta/main.yaml new file mode 100644 index 00000000000..e5c8cd02f04 --- /dev/null +++ b/test/integration/targets/eos_static_routes/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_eos_tests diff --git a/test/integration/targets/eos_static_routes/tasks/cli.yaml b/test/integration/targets/eos_static_routes/tasks/cli.yaml new file mode 100644 index 00000000000..5b16f46bde2 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tasks/cli.yaml @@ -0,0 +1,17 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_static_routes/tasks/eapi.yaml b/test/integration/targets/eos_static_routes/tasks/eapi.yaml new file mode 100644 index 00000000000..1109c6bf6ac --- /dev/null +++ b/test/integration/targets/eos_static_routes/tasks/eapi.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all eapi test cases + find: + paths: "{{ role_path }}/tests/eapi" + patterns: "{{ testcase }}.yaml" + delegate_to: localhost + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include: "{{ test_case_to_run }} ansible_connection=httpapi" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_static_routes/tasks/main.yaml b/test/integration/targets/eos_static_routes/tasks/main.yaml new file mode 100644 index 00000000000..970e74171ea --- /dev/null +++ b/test/integration/targets/eos_static_routes/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- { include: cli.yaml, tags: ['cli'] } +- { include: eapi.yaml, tags: ['eapi'] } diff --git a/test/integration/targets/eos_static_routes/tests/cli/_parsed.cfg b/test/integration/targets/eos_static_routes/tests/cli/_parsed.cfg new file mode 100644 index 00000000000..a4bb4d65496 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/_parsed.cfg @@ -0,0 +1,7 @@ +ip route 10.1.1.0/24 Management1 +ip route 10.1.1.0/24 Ethernet1 20.1.1.3 track bfd 200 +ip route 10.50.0.0/16 Management1 +ip route 23.1.0.0/16 Nexthop-Group testgrp tag 42 +ip route vrf testvrf 120.1.1.0/24 Ethernet1 23 +ip route vrf vrftest1 77.77.1.0/24 33.1.1.1 +ipv6 route 1000:10::/64 Ethernet1 67 tag 98 diff --git a/test/integration/targets/eos_static_routes/tests/cli/_populate.yaml b/test/integration/targets/eos_static_routes/tests/cli/_populate.yaml new file mode 100644 index 00000000000..98f1ff97257 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/_populate.yaml @@ -0,0 +1,16 @@ +--- +- name: Setup + cli_config: + config: "{{ lines }}" + become: yes + vars: + lines: | + vrf definition testvrf + vrf definition vrftest1 + ip route 10.1.1.0/24 Management1 + ip route 10.1.1.0/24 Ethernet1 20.1.1.3 track bfd 200 + ip route 10.50.0.0/16 Management1 + ip route 23.1.0.0/16 Nexthop-Group testgrp tag 42 + ip route vrf testvrf 120.1.1.0/24 Ethernet1 23 + ip route vrf vrftest1 77.77.1.0/24 33.1.1.1 + ipv6 route 1000:10::/64 Ethernet1 67 tag 98 diff --git a/test/integration/targets/eos_static_routes/tests/cli/_remove_config.yaml b/test/integration/targets/eos_static_routes/tests/cli/_remove_config.yaml new file mode 100644 index 00000000000..aa95912d80e --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/_remove_config.yaml @@ -0,0 +1,20 @@ +--- +- name: Setup + cli_config: + config: "{{ lines }}" + become: yes + vars: + lines: | + no vrf definition testvrf + no vrf definition vrftest1 + no ip route 10.1.1.0/24 Management1 + no ip route 10.1.1.0/24 Ethernet1 20.1.1.3 track bfd 200 + no ip route 10.50.0.0/16 Management1 + no ip route 23.1.0.0/16 Nexthop-Group testgrp tag 42 + no ip route 155.55.1.0/24 Nexthop-Group testgrp tag 100 + no ip route 122.1.19.0/24 Nexthop-Group testgrp 21 + no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23 + no ip route vrf vrftest1 77.77.1.0/24 33.1.1.1 + no ipv6 route 1000:10::/64 Ethernet1 67 tag 98 + no ipv6 route vrf testvrf 1120:10::/64 Ethernet1 55 + no ipv6 route 1000:10::/64 Ethernet1 55 diff --git a/test/integration/targets/eos_static_routes/tests/cli/deleted.yaml b/test/integration/targets/eos_static_routes/tests/cli/deleted.yaml new file mode 100644 index 00000000000..3b5dcb57394 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/deleted.yaml @@ -0,0 +1,224 @@ +--- +- debug: + msg: "Start eos_static_routes deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + +- set_fact: + config1: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - interface: Ethernet1 + admin_distance: 23 + vrf: testvrf + +- set_fact: + config2: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + vrf: testvrf + +- set_fact: + config3: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - interface: Ethernet1 + admin_distance: 23 + vrf: testvrf + + +- name: Delete attributes of given static routes - dest specific. + eos_static_routes: &deleted + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv4' + routes: + - dest: '120.1.1.0/24' + - address_families: + - afi: 'ipv6' + routes: + - dest: '1000:10::/64' + state: deleted + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config) == []" + - '"no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23" in result.commands' + - '"no ipv6 route 1000:10::/64 Ethernet1 67 tag 98" in result.commands' + become: yes + +- name: Idempotency check + eos_static_routes: *deleted + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + +- include_tasks: _populate.yaml + +- name: Delete attributes of given static routes - afi specific. + eos_static_routes: + config: + - address_families: + - afi: 'ipv6' + state: deleted + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config1) == []" + - '"no ipv6 route 1000:10::/64 Ethernet1 67 tag 98" in result.commands' + become: yes + +- name: Delete attributes of given static routes - vrf specific. + eos_static_routes: + config: + - vrf: vrftest1 + state: deleted + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config2) == []" + - '"no ip route vrf vrftest1 77.77.1.0/24 33.1.1.1" in result.commands' + become: yes + +- name: Delete attributes of given static routes - nexthop specific. + eos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + state: deleted + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config3) == []" + - '"no ip route 10.1.1.0/24 Management1" in result.commands' + become: yes + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/gathered.yaml b/test/integration/targets/eos_static_routes/tests/cli/gathered.yaml new file mode 100644 index 00000000000..f89a6c08092 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/gathered.yaml @@ -0,0 +1,75 @@ +--- +- debug: + msg: "START eos_static_routes gathered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate.yaml + +- block: + - name: Gathered the provided configuration with the exisiting running configuration + eos_static_routes: &gathered + config: + state: gathered + become: yes + register: result + + - set_fact: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 67 + interface: Ethernet1 + tag: 98 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + vrf: testvrf + + - name: Assert that gathered dicts was correctly generated + assert: + that: + - " config | symmetric_difference(result['gathered']) == []" + + - name: Gather the existing running configuration (IDEMPOTENT) + eos_static_routes: *gathered + become: yes + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/merged.yaml b/test/integration/targets/eos_static_routes/tests/cli/merged.yaml new file mode 100644 index 00000000000..55ecdb5b847 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/merged.yaml @@ -0,0 +1,102 @@ +--- +- debug: + msg: "Start eos_static_routes merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - dest: 155.55.1.0/24 + next_hops: + - nexthop_grp: testgrp + tag: 100 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 67 + interface: Ethernet1 + tag: 98 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + - afi: ipv6 + routes: + - dest: 1120:10::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + vrf: testvrf + +- name: merge attributes of given static routes. + eos_static_routes: &merged + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv6' + routes: + - dest: '1120:10::/64' + next_hops: + - interface: Ethernet1 + admin_distance: 55 + - address_families: + - afi: 'ipv4' + routes: + - dest: '155.55.1.0/24' + next_hops: + - nexthop_grp: testgrp + tag: 100 + state: merged + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config) == []" + - '"ipv6 route vrf testvrf 1120:10::/64 Ethernet1 55" in result.commands' + - '"ip route 155.55.1.0/24 Nexthop-Group testgrp tag 100" in result.commands' + become: yes + +- name: Idempotency check + eos_static_routes: *merged + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/overridden.yaml b/test/integration/targets/eos_static_routes/tests/cli/overridden.yaml new file mode 100644 index 00000000000..5af837fdf0c --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/overridden.yaml @@ -0,0 +1,61 @@ +--- +- debug: + msg: "Start eos_static_routes merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - address_families: + - afi: ipv6 + routes: + - dest: 1120:10::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + vrf: testvrf + +- name: Override attributes of given static routes. + eos_static_routes: &overridden + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv6' + routes: + - dest: '1120:10::/64' + next_hops: + - interface: Ethernet1 + admin_distance: 55 + state: overridden + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config) == []" + - "result.commands|length == 8" + - '"no ipv6 route 1000:10::/64 Ethernet1 67 tag 98" in result.commands' + - '"no ip route 23.1.0.0/16 Nexthop-Group testgrp tag 42" in result.commands' + - '"no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23" in result.commands' + - '"no ip route 10.50.0.0/16 Management1" in result.commands' + - '"no ip route 10.1.1.0/24 Management1" in result.commands' + - '"no ip route 10.1.1.0/24 Ethernet1 20.1.1.3 track bfd 200" in result.commands' + - '"no ip route vrf vrftest1 77.77.1.0/24 33.1.1.1" in result.commands' + - '"ipv6 route vrf testvrf 1120:10::/64 Ethernet1 55" in result.commands' + become: yes + +- name: Idempotency check + eos_static_routes: *overridden + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/parsed.yaml b/test/integration/targets/eos_static_routes/tests/cli/parsed.yaml new file mode 100644 index 00000000000..aaaabb449a0 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/parsed.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: "START eos_static_routes parsed integration tests on connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- name: Gather static_routes facts + eos_facts: + gather_subset: + - default + gather_network_resources: + - static_routes + become: yes + register: static_routes_facts + +- name: Provide the running configuration for parsing (config to be parsed) + eos_static_routes: &parsed + running_config: + "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + become: yes + register: result + +- assert: + that: + - "{{ ansible_facts['network_resources']['static_routes'] | symmetric_difference(result['parsed']) |length == 0 }}" + +- name: Gather the existing running configuration (IDEMPOTENT) + eos_static_routes: *parsed + become: yes + register: result + +- assert: + that: + - "result['changed'] == false" + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/rendered.yaml b/test/integration/targets/eos_static_routes/tests/cli/rendered.yaml new file mode 100644 index 00000000000..d188702d48a --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/rendered.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: "START eos_static_routes rendered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate.yaml + +- block: + - name: Structure provided configuration into device specific commands + eos_static_routes: &rendered + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv6' + routes: + - dest: '1120:10::/64' + next_hops: + - interface: Ethernet1 + admin_distance: 55 + - address_families: + - afi: 'ipv4' + routes: + - dest: '155.55.1.0/24' + next_hops: + - nexthop_grp: testgrp + tag: 100 + state: rendered + become: yes + register: result + + + - name: Assert that correct set of commands were generated + vars: + lines: + - ipv6 route vrf testvrf 1120:10::/64 Ethernet1 55 + - ip route 155.55.1.0/24 Nexthop-Group testgrp tag 100 + + assert: + that: + - "{{ lines | symmetric_difference(result['rendered']) |length == 0 }}" + + - name: Structure provided configuration into device specific commands (IDEMPOTENT) + eos_static_routes: *rendered + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/replaced.yaml b/test/integration/targets/eos_static_routes/tests/cli/replaced.yaml new file mode 100644 index 00000000000..1a8353803ec --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/replaced.yaml @@ -0,0 +1,90 @@ +--- +- debug: + msg: "Start eos_static_routes merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 67 + interface: Ethernet1 + tag: 98 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + vrf: testvrf + +- name: Replace attributes of given static routes. + eos_static_routes: &replaced + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv6' + routes: + - dest: '1000:10::/64' + next_hops: + - interface: Ethernet1 + admin_distance: 55 + state: replaced + become: yes + register: result + +- eos_facts: + gather_network_resources: static_routes + become: yes + +- assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config) == []" + - '"ipv6 route vrf testvrf 1000:10::/64 Ethernet1 55" in result.commands' + become: yes + +- name: Idempotency check + eos_static_routes: *replaced + become: yes + register: result + +- assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + +- include_tasks: _remove_config.yaml diff --git a/test/integration/targets/eos_static_routes/tests/cli/rtt.yaml b/test/integration/targets/eos_static_routes/tests/cli/rtt.yaml new file mode 100644 index 00000000000..1890b1f8335 --- /dev/null +++ b/test/integration/targets/eos_static_routes/tests/cli/rtt.yaml @@ -0,0 +1,177 @@ +--- +- debug: + msg: "Start eos_static_routes merged integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _populate.yaml + +- set_fact: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - dest: 155.55.1.0/24 + next_hops: + - nexthop_grp: testgrp + tag: 100 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 67 + interface: Ethernet1 + tag: 98 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + - afi: ipv6 + routes: + - dest: 1120:10::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + vrf: testvrf + + + revert_config: + - address_families: + - afi: ipv4 + routes: + - dest: 10.1.1.0/24 + next_hops: + - interface: Management1 + - admin_distance: 200 + forward_router_address: 20.1.1.3 + interface: Ethernet1 + track: bfd + - dest: 10.50.0.0/16 + next_hops: + - interface: Management1 + - dest: 23.1.0.0/16 + next_hops: + - nexthop_grp: testgrp + tag: 42 + - dest: 122.1.19.0/24 + next_hops: + - admin_distance: 21 + nexthop_grp: testgrp + - dest: 155.55.1.0/24 + next_hops: + - nexthop_grp: testgrp + tag: 100 + - afi: ipv6 + routes: + - dest: 1000:10::/64 + next_hops: + - admin_distance: 67 + interface: Ethernet1 + tag: 98 + - address_families: + - afi: ipv4 + routes: + - dest: 77.77.1.0/24 + next_hops: + - interface: 33.1.1.1 + vrf: vrftest1 + - address_families: + - afi: ipv4 + routes: + - dest: 120.1.1.0/24 + next_hops: + - admin_distance: 23 + interface: Ethernet1 + - afi: ipv6 + routes: + - dest: 1120:10::/64 + next_hops: + - admin_distance: 55 + interface: Ethernet1 + vrf: testvrf + +- block: + - name: merge attributes of given static routes. + eos_static_routes: + config: + - vrf: "testvrf" + address_families: + - afi: 'ipv6' + routes: + - dest: '1120:10::/64' + next_hops: + - interface: Ethernet1 + admin_distance: 55 + - address_families: + - afi: 'ipv4' + routes: + - dest: '155.55.1.0/24' + next_hops: + - nexthop_grp: testgrp + tag: 100 + state: merged + become: yes + register: base_config + + - eos_facts: + gather_network_resources: static_routes + become: yes + + - assert: + that: + - "ansible_facts.network_resources.static_routes|symmetric_difference(config) == []" + + - name: Apply the provided configuration (config to be reverted) + eos_static_routes: + config: + - address_families: + - afi: 'ipv4' + routes: + - dest: '122.1.19.0/24' + next_hops: + - nexthop_grp: testgrp + admin_distance: 21 + state: merged + become: yes + register: result + + - assert: + that: + - "result.after|symmetric_difference(revert_config) == []" + + - name: Revert back to base config using facts round trip + eos_static_routes: + config: "{{ ansible_facts['network_resources']['static_routes'] }}" + state: overridden + become: yes + register: revert + + - name: Assert that config was reverted + assert: + that: + - "ansible_facts.network_resources.static_routes | symmetric_difference(revert.after) == []" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 58ff0c94f83..8b3d25440ab 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -4509,13 +4509,13 @@ lib/ansible/modules/network/eos/eos_logging.py validate-modules:doc-required-mis lib/ansible/modules/network/eos/eos_logging.py validate-modules:missing-suboption-docs lib/ansible/modules/network/eos/eos_logging.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/network/eos/eos_logging.py validate-modules:undocumented-parameter -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:doc-choices-do-not-match-spec -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:doc-elements-mismatch -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:doc-missing-type -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:doc-required-mismatch -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:missing-suboption-docs -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:undocumented-parameter +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:doc-choices-do-not-match-spec +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:doc-elements-mismatch +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:doc-missing-type +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:doc-required-mismatch +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:missing-suboption-docs +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:parameter-type-not-in-doc +lib/ansible/modules/network/eos/_eos_static_route.py validate-modules:undocumented-parameter lib/ansible/modules/network/eos/eos_system.py future-import-boilerplate lib/ansible/modules/network/eos/eos_system.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_system.py validate-modules:doc-missing-type diff --git a/test/units/modules/network/eos/fixtures/eos_static_routes_config.cfg b/test/units/modules/network/eos/fixtures/eos_static_routes_config.cfg new file mode 100644 index 00000000000..6ec815d7dc0 --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_static_routes_config.cfg @@ -0,0 +1,3 @@ +ip route 10.1.1.0/24 Management1 +ip route vrf testvrf 120.1.1.0/24 Ethernet1 23 +ipv6 route 1000:10::/64 Ethernet1 67 tag 98 diff --git a/test/units/modules/network/eos/fixtures/eos_static_routes_config1.cfg b/test/units/modules/network/eos/fixtures/eos_static_routes_config1.cfg new file mode 100644 index 00000000000..db829ee8c1c --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_static_routes_config1.cfg @@ -0,0 +1 @@ +ip route 10.1.1.0/24 Management1 diff --git a/test/units/modules/network/eos/test_eos_static_routes.py b/test/units/modules/network/eos/test_eos_static_routes.py new file mode 100644 index 00000000000..03d2fb0f74b --- /dev/null +++ b/test/units/modules/network/eos/test_eos_static_routes.py @@ -0,0 +1,308 @@ +# +# (c) 2019, 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 units.compat.mock import patch +from ansible.modules.network.eos import eos_static_routes +from ansible.module_utils.network.eos.config.static_routes.static_routes import add_commands +from units.modules.utils import set_module_args +from .eos_module import TestEosModule, load_fixture +import itertools + + +class TestEosStaticRoutesModule(TestEosModule): + module = eos_static_routes + + def setUp(self): + super(TestEosStaticRoutesModule, self).setUp() + + self.mock_get_config = patch( + 'ansible.module_utils.network.common.network.Config.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + 'ansible.module_utils.network.common.network.Config.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_get_resource_connection_config = patch( + 'ansible.module_utils.network.common.cfg.base.get_resource_connection' + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start( + ) + + self.mock_get_resource_connection_facts = patch( + 'ansible.module_utils.network.common.facts.facts.get_resource_connection' + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start( + ) + + self.mock_edit_config = patch( + 'ansible.module_utils.network.eos.providers.providers.CliProvider.edit_config' + ) + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch( + 'ansible.module_utils.network.eos.facts.static_routes.static_routes.Static_routesFacts.get_device_data' + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestEosStaticRoutesModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_edit_config.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None, transport='cli', filename=None): + if filename is None: + filename = 'eos_static_routes_config.cfg' + + def load_from_file(*args, **kwargs): + output = load_fixture(filename) + return output + + self.execute_show_command.side_effect = load_from_file + + def test_eos_static_routes_merged(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1200:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=55) + ]) + ]) + ]) + ], state="merged")) + commands = ['ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55'] + result = self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_merged_idempotent(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv4", + routes=[ + dict(dest="120.1.1.0/24", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=23) + ]) + ]) + ]) + ], state="merged")) + self.execute_module(changed=False, commands=[]) + + def test_eos_static_routes_default(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1200:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=55) + ]) + ]) + ]) + ])) + commands = ['ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55'] + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_default_idempotent(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv4", + routes=[ + dict(dest="120.1.1.0/24", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=23) + ]) + ]) + ]) + ])) + self.execute_module(changed=False, commands=[]) + + def test_eos_static_routes_replaced(self): + set_module_args( + dict(config=[ + dict(address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1000:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=55) + ]) + ]) + ]) + ], state="replaced")) + commands = [ + 'ipv6 route 1000:10::/64 Ethernet1 55', + 'no ipv6 route 1000:10::/64 Ethernet1 67 tag 98' + ] + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_replaced_idempotent(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv4", + routes=[ + dict(dest="120.1.1.0/24", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=23) + ]) + ]) + ]) + ], state="replaced")) + self.execute_module(changed=False, commands=[]) + + def test_eos_static_routes_overridden(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1200:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=55) + ]) + ]) + ]) + ], state="overridden")) + commands = [ + 'ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55', + 'no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23', + 'no ip route 10.1.1.0/24 Management1', + 'no ipv6 route 1000:10::/64 Ethernet1 67 tag 98' + ] + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_overridden_idempotent(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv4", + routes=[ + dict(dest="120.1.1.0/24", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=23) + ]) + ]) + ]), + dict(address_families=[ + dict(afi="ipv4", + routes=[ + dict(dest="10.1.1.0/24", + next_hops=[ + dict(interface="Management1") + ]) + ]) + ]), + dict(address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1000:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=67, + tag=98) + ]) + ]) + ]) + ], state="overridden")) + self.execute_module(changed=False, commands=[]) + + def test_eos_static_routes_deletedvrf(self): + set_module_args(dict(config=[dict(vrf="testvrf", )], state="deleted")) + commands = ['no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23'] + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_deletedroute(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv4", routes=[dict(dest="120.1.1.0/24")]) + ]) + ], state="deleted")) + commands = ['no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23'] + + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_deletedafi(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", address_families=[dict(afi="ipv4")]) + ], state="deleted")) + commands = ['no ip route vrf testvrf 120.1.1.0/24 Ethernet1 23'] + self.execute_module(changed=True, commands=commands) + + def test_eos_static_routes_gathered(self): + set_module_args( + dict(config=[], + state="gathered")) + result = self.execute_module(changed=False, filename='eos_static_routes_config.cfg') + commands = [] + for gathered_cmds in result['gathered']: + cfg = add_commands(gathered_cmds) + commands.append(cfg) + commands = list(itertools.chain(*commands)) + config_commands = ['ip route 10.1.1.0/24 Management1', 'ipv6 route 1000:10::/64 Ethernet1 67 tag 98', + 'ip route vrf testvrf 120.1.1.0/24 Ethernet1 23'] + self.assertEqual(sorted(config_commands), sorted(commands), result['gathered']) + + def test_eos_static_routes_rendered(self): + set_module_args( + dict(config=[ + dict(vrf="testvrf", + address_families=[ + dict(afi="ipv6", + routes=[ + dict(dest="1200:10::/64", + next_hops=[ + dict(interface="Ethernet1", + admin_distance=55) + ]) + ]) + ]) + ], state="rendered")) + commands = ['ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55'] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), sorted(commands), result['rendered']) + + def test_eos_static_routes_parsed(self): + set_module_args( + dict(running_config="ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55", + state="parsed")) + commands = ['ipv6 route vrf testvrf 1200:10::/64 Ethernet1 55'] + result = self.execute_module(changed=False) + parsed_commands = [] + for cmds in result['parsed']: + cfg = add_commands(cmds) + parsed_commands.append(cfg) + parsed_commands = list(itertools.chain(*parsed_commands)) + self.assertEqual(sorted(parsed_commands), sorted(commands), result['parsed'])