From 3405ee1c0142b98258f4dcfe548049dbe39c61e3 Mon Sep 17 00:00:00 2001 From: Nilashish Chakraborty Date: Thu, 27 Feb 2020 14:31:00 +0530 Subject: [PATCH] Add iosxr_static_routes RM (#65181) Signed-off-by: NilashishC --- .../iosxr/argspec/static_routes/__init__.py | 0 .../argspec/static_routes/static_routes.py | 121 +++ .../iosxr/config/static_routes/__init__.py | 0 .../config/static_routes/static_routes.py | 560 ++++++++++ .../module_utils/network/iosxr/facts/facts.py | 4 +- .../iosxr/facts/static_routes/__init__.py | 0 .../facts/static_routes/static_routes.py | 176 ++++ .../modules/network/iosxr/iosxr_facts.py | 2 +- .../network/iosxr/iosxr_static_routes.py | 966 ++++++++++++++++++ .../iosxr_static_routes/defaults/main.yaml | 3 + .../iosxr_static_routes/fixtures/parsed.cfg | 18 + .../iosxr_static_routes/tasks/cli.yaml | 20 + .../iosxr_static_routes/tasks/main.yaml | 2 + .../tests/cli/_populate_config.yaml | 60 ++ .../tests/cli/_remove_config.yaml | 8 + .../tests/cli/deleted.yaml | 124 +++ .../tests/cli/empty_config.yaml | 47 + .../tests/cli/gathered.yaml | 20 + .../iosxr_static_routes/tests/cli/merged.yaml | 141 +++ .../tests/cli/overridden.yaml | 66 ++ .../iosxr_static_routes/tests/cli/parsed.yaml | 15 + .../tests/cli/rendered.yaml | 76 ++ .../tests/cli/replaced.yaml | 58 ++ .../iosxr_static_routes/tests/cli/rtt.yaml | 73 ++ .../iosxr_static_routes/vars/main.yaml | 281 +++++ .../fixtures/iosxr_static_routes_config.cfg | 18 + .../network/iosxr/test_iosxr_static_routes.py | 395 +++++++ 27 files changed, 3252 insertions(+), 2 deletions(-) create mode 100644 lib/ansible/module_utils/network/iosxr/argspec/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/argspec/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/iosxr/config/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/config/static_routes/static_routes.py create mode 100644 lib/ansible/module_utils/network/iosxr/facts/static_routes/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/facts/static_routes/static_routes.py create mode 100644 lib/ansible/modules/network/iosxr/iosxr_static_routes.py create mode 100644 test/integration/targets/iosxr_static_routes/defaults/main.yaml create mode 100644 test/integration/targets/iosxr_static_routes/fixtures/parsed.cfg create mode 100644 test/integration/targets/iosxr_static_routes/tasks/cli.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tasks/main.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/_populate_config.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/_remove_config.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/deleted.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/empty_config.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/gathered.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/merged.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/overridden.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/parsed.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/rendered.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/replaced.yaml create mode 100644 test/integration/targets/iosxr_static_routes/tests/cli/rtt.yaml create mode 100644 test/integration/targets/iosxr_static_routes/vars/main.yaml create mode 100644 test/units/modules/network/iosxr/fixtures/iosxr_static_routes_config.cfg create mode 100644 test/units/modules/network/iosxr/test_iosxr_static_routes.py diff --git a/lib/ansible/module_utils/network/iosxr/argspec/static_routes/__init__.py b/lib/ansible/module_utils/network/iosxr/argspec/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/argspec/static_routes/static_routes.py b/lib/ansible/module_utils/network/iosxr/argspec/static_routes/static_routes.py new file mode 100644 index 00000000000..030515b8779 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/argspec/static_routes/static_routes.py @@ -0,0 +1,121 @@ +# +# -*- 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 iosxr_static_routes module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Static_routesArgs(object): # pylint: disable=R0903 + """The arg spec for the iosxr_static_routes module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'vrf': { + 'type': 'str' + }, + 'address_families': { + 'elements': 'dict', + 'options': { + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + }, + 'safi': { + 'choices': ['unicast', 'multicast'], + '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' + }, + 'dest_vrf': { + 'type': 'str' + }, + 'forward_router_address': { + 'type': 'str' + }, + 'interface': { + 'type': 'str' + }, + 'metric': { + 'type': 'int' + }, + 'tag': { + 'type': 'int' + }, + 'track': { + 'type': 'str' + }, + 'tunnel_id': { + 'type': 'int' + }, + 'vrflabel': { + 'type': 'int' + } + }, + 'type': 'list' + } + }, + 'type': 'list' + }, + }, + 'type': 'list' + }, + }, + 'type': 'list' + }, + 'running_config': { + 'type': 'str' + }, + 'state': { + 'choices': [ + 'merged', 'replaced', 'overridden', 'deleted', 'gathered', 'rendered', 'parsed' + ], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/iosxr/config/static_routes/__init__.py b/lib/ansible/module_utils/network/iosxr/config/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/config/static_routes/static_routes.py b/lib/ansible/module_utils/network/iosxr/config/static_routes/static_routes.py new file mode 100644 index 00000000000..1c843b91663 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/config/static_routes/static_routes.py @@ -0,0 +1,560 @@ +# +# -*- 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 iosxr_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 + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.iosxr.facts.facts import Facts +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import ( + search_obj_in_list, + remove_empties, + dict_diff, + dict_merge, +) + + +class Static_routes(ConfigBase): + """ + The iosxr_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: + self._connection.edit_config(commands) + 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": + running_config = self._module.params["running_config"] + if not 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=running_config) + + 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 + """ + want = self._module.params["config"] + have = existing_static_routes_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params["state"] + commands = [] + + if state in ("overridden", "merged", "replaced", "rendered") and not want: + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format( + state + ) + ) + + if state == "overridden": + commands.extend(self._state_overridden(want, have)) + + elif state == "deleted": + if not want: + if len(have) >= 1: + return "no router static" + + else: + for w_item in want: + obj_in_have = self._find_vrf(w_item, have) + if obj_in_have: + commands.extend( + self._state_deleted(remove_empties(w_item), obj_in_have) + ) + + else: + for w_item in want: + obj_in_have = self._find_vrf(w_item, have) + if state == "merged" or self.state == "rendered": + commands.extend( + self._state_merged(remove_empties(w_item), obj_in_have) + ) + + elif state == "replaced": + commands.extend( + self._state_replaced(remove_empties(w_item), obj_in_have) + ) + + if commands: + commands.insert(0, "router static") + + return commands + + def _state_replaced(self, 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 = [] + + for want_afi in want.get("address_families", []): + have_afi = ( + self.find_af_context(want_afi, have.get("address_families", [])) or {} + ) + update_commands = [] + for want_route in want_afi.get("routes", []): + have_route = ( + search_obj_in_list( + want_route["dest"], have_afi.get("routes", []), key="dest" + ) + or {} + ) + + rotated_have_next_hops = self.rotate_next_hops( + have_route.get("next_hops", {}) + ) + rotated_want_next_hops = self.rotate_next_hops( + want_route.get("next_hops", {}) + ) + + for key in rotated_have_next_hops.keys(): + if key not in rotated_want_next_hops: + cmd = "no {0}".format(want_route["dest"]) + for item in key: + if "." in item or ":" in item or "/" in item: + cmd += " {0}".format(item) + else: + cmd += " vrf {0}".format(item) + update_commands.append(cmd) + + for key, value in iteritems(rotated_want_next_hops): + if key in rotated_have_next_hops: + existing = True + have_exit_point_attribs = rotated_have_next_hops[key] + + else: + existing = False + have_exit_point_attribs = {} + + updates = dict_diff(have_exit_point_attribs, value) + + if updates or not existing: + update_commands.append( + self._compute_commands( + dest=want_route["dest"], next_hop=key, updates=updates + ) + ) + + if update_commands: + update_commands.insert( + 0, + "address-family {0} {1}".format(want_afi["afi"], want_afi["safi"]), + ) + commands.extend(update_commands) + + if "vrf" in want and update_commands: + commands.insert(0, "vrf {0}".format(want["vrf"])) + + return commands + + def _state_overridden(self, 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 = [] + + # Iterate through all the entries, i.e., VRFs and Global entry in have + # and fully remove the ones that are not present in want and then call + # replaced + + for h_item in have: + w_item = self._find_vrf(h_item, want) + + # Delete all the top-level keys (VRFs/Global Route Entry) that are + # not specified in want. + if not w_item: + if "vrf" in h_item: + commands.append("no vrf {0}".format(h_item["vrf"])) + else: + for have_afi in h_item.get("address_families", []): + commands.append( + "no address-family {0} {1}".format( + have_afi["afi"], have_afi["safi"] + ) + ) + + # For VRFs/Global Entry present in want, we also need to delete extraneous routes + # from them. We cannot reuse `_state_replaced` for this purpose since its scope is + # limited to replacing a single `dest`. + else: + del_cmds = [] + for have_afi in h_item.get("address_families", []): + want_afi = ( + self.find_af_context( + have_afi, w_item.get("address_families", []) + ) + or {} + ) + update_commands = [] + for h_route in have_afi.get("routes", []): + w_route = ( + search_obj_in_list( + h_route["dest"], want_afi.get("routes", []), key="dest" + ) + or {} + ) + if not w_route: + update_commands.append("no {0}".format(h_route["dest"])) + + if update_commands: + update_commands.insert( + 0, + "address-family {0} {1}".format( + want_afi["afi"], want_afi["safi"] + ), + ) + del_cmds.extend(update_commands) + + if "vrf" in want and update_commands: + del_cmds.insert(0, "vrf {0}".format(want["vrf"])) + + commands.extend(del_cmds) + + # We finally call `_state_replaced` to replace exiting `dest` entries + # or add new ones as specified in want. + for w_item in want: + h_item = self._find_vrf(w_item, have) + commands.extend(self._state_replaced(remove_empties(w_item), h_item)) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + + for want_afi in want.get("address_families", []): + have_afi = ( + self.find_af_context(want_afi, have.get("address_families", [])) or {} + ) + + update_commands = [] + for want_route in want_afi.get("routes", []): + have_route = ( + search_obj_in_list( + want_route["dest"], have_afi.get("routes", []), key="dest" + ) + or {} + ) + + # convert the next_hops list of dictionaries to dictionary of + # dictionaries with (`dest_vrf`, `forward_router_address`, `interface`) tuple + # being the key for each dictionary. + # a combination of these 3 attributes uniquely identifies a route entry. + # in case `dest_vrf` is not specified, `forward_router_address` and `interface` + # become the unique identifier + rotated_have_next_hops = self.rotate_next_hops( + have_route.get("next_hops", {}) + ) + rotated_want_next_hops = self.rotate_next_hops( + want_route.get("next_hops", {}) + ) + + # for every dict in the want next_hops dictionaries, if the key + # is present in `rotated_have_next_hops`, we set `existing` to True, + # which means the the given want exit point exists and we run dict_diff + # on `value` which is basically all the other attributes of the exit point + # if the key is not present, it means that this is a new exit point + for key, value in iteritems(rotated_want_next_hops): + if key in rotated_have_next_hops: + existing = True + have_exit_point_attribs = rotated_have_next_hops[key] + + else: + existing = False + have_exit_point_attribs = {} + + updates = dict_diff(have_exit_point_attribs, value) + if updates or not existing: + update_commands.append( + self._compute_commands( + dest=want_route["dest"], + next_hop=key, + # dict_merge() is necessary to make sure that we + # don't end up overridding the entry and also to + # allow incremental updates + updates=dict_merge( + rotated_have_next_hops.get(key, {}), updates + ), + ) + ) + + if update_commands: + update_commands.insert( + 0, + "address-family {0} {1}".format(want_afi["afi"], want_afi["safi"]), + ) + commands.extend(update_commands) + + if "vrf" in want and update_commands: + commands.insert(0, "vrf {0}".format(want["vrf"])) + + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + if "address_families" not in want: + return ["no vrf {0}".format(want["vrf"])] + + else: + for want_afi in want.get("address_families", []): + update_commands = [] + have_afi = ( + self.find_af_context(want_afi, have.get("address_families", [])) + or {} + ) + if have_afi: + if "routes" not in want_afi: + commands.append( + "no address-family {0} {1}".format( + have_afi["afi"], have_afi["safi"] + ) + ) + else: + for want_route in want_afi.get("routes", []): + have_route = ( + search_obj_in_list( + want_route["dest"], + have_afi.get("routes", []), + key="dest", + ) + or {} + ) + if have_route: + if "next_hops" not in want_route: + update_commands.append( + "no {0}".format(want_route["dest"]) + ) + else: + rotated_have_next_hops = self.rotate_next_hops( + have_route.get("next_hops", {}) + ) + rotated_want_next_hops = self.rotate_next_hops( + want_route.get("next_hops", {}) + ) + + for key in rotated_want_next_hops.keys(): + if key in rotated_have_next_hops: + cmd = "no {0}".format(want_route["dest"]) + for item in key: + if ( + "." in item + or ":" in item + or "/" in item + ): + cmd += " {0}".format(item) + else: + cmd += " vrf {0}".format(item) + update_commands.append(cmd) + + if update_commands: + update_commands.insert( + 0, + "address-family {0} {1}".format( + want_afi["afi"], want_afi["safi"] + ), + ) + commands.extend(update_commands) + + if "vrf" in want and commands: + commands.insert(0, "vrf {0}".format(want["vrf"])) + + return commands + + def _find_vrf(self, item, entries): + """ This method iterates through the items + in `entries` and returns the object that + matches `item`. + + :rtype: A dict + :returns: the obj in `entries` that matches `item` + """ + obj = {} + afi = item.get("vrf") + + if afi: + obj = search_obj_in_list(afi, entries, key="vrf") or {} + else: + for x in entries: + if "vrf" not in remove_empties(x): + obj = x + break + return obj + + def find_af_context(self, want_af_context, have_address_families): + """ This method iterates through the have AFs + and returns the one that matches the want AF + + :rtype: A dict + :returns: the corresponding AF in have AFs + that matches the want AF + """ + for have_af in have_address_families: + if ( + have_af["afi"] == want_af_context["afi"] + and have_af["safi"] == want_af_context["safi"] + ): + return have_af + + def rotate_next_hops(self, next_hops): + """ This method iterates through the list of + next hops for a given destination network + and converts it to a dictionary of dictionaries. + Each dictionary has a primary key indicated by the + tuple of `dest_vrf`, `forward_router_address` and + `interface` and the value of this key is a dictionary + that contains all the other attributes of the next hop. + + :rtype: A dict + :returns: A next_hops list in a dictionary of dictionaries format + """ + next_hops_dict = {} + + for entry in next_hops: + entry = entry.copy() + key_list = [] + + for x in ["dest_vrf", "forward_router_address", "interface"]: + if entry.get(x): + key_list.append(entry.pop(x)) + + key = tuple(key_list) + next_hops_dict[key] = entry + + return next_hops_dict + + def _compute_commands(self, dest, next_hop, updates=None): + """ This method computes a static route entry command + from the specified `dest`, `next_hop` and `updates` + + :rtype: A str + :returns: A platform specific static routes command + """ + if not updates: + updates = {} + + command = dest + + for x in next_hop: + if "." in x or ":" in x or "/" in x: + command += " {0}".format(x) + else: + command += " vrf {0}".format(x) + + for key in sorted(updates): + if key == "admin_distance": + command += " {0}".format(updates[key]) + else: + command += " {0} {1}".format(key, updates[key]) + + return command diff --git a/lib/ansible/module_utils/network/iosxr/facts/facts.py b/lib/ansible/module_utils/network/iosxr/facts/facts.py index 312682f1ad2..109a813a598 100644 --- a/lib/ansible/module_utils/network/iosxr/facts/facts.py +++ b/lib/ansible/module_utils/network/iosxr/facts/facts.py @@ -25,6 +25,7 @@ from ansible.module_utils.network.iosxr.facts.l2_interfaces.l2_interfaces import from ansible.module_utils.network.iosxr.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts from ansible.module_utils.network.iosxr.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts from ansible.module_utils.network.iosxr.facts.acls.acls import AclsFacts +from ansible.module_utils.network.iosxr.facts.static_routes.static_routes import Static_routesFacts FACT_LEGACY_SUBSETS = dict( @@ -43,7 +44,8 @@ FACT_RESOURCE_SUBSETS = dict( lag_interfaces=Lag_interfacesFacts, l3_interfaces=L3_InterfacesFacts, acl_interfaces=Acl_interfacesFacts, - acls=AclsFacts + acls=AclsFacts, + static_routes=Static_routesFacts ) diff --git a/lib/ansible/module_utils/network/iosxr/facts/static_routes/__init__.py b/lib/ansible/module_utils/network/iosxr/facts/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/facts/static_routes/static_routes.py b/lib/ansible/module_utils/network/iosxr/facts/static_routes/static_routes.py new file mode 100644 index 00000000000..3485cc7813d --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/facts/static_routes/static_routes.py @@ -0,0 +1,176 @@ +# +# -*- 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 iosxr 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.iosxr.argspec.static_routes.static_routes import Static_routesArgs + + +class Static_routesFacts(object): + """ The iosxr 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_config(flags="router static") + + 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) + + objs = [] + + if "No such configuration" not in data: + for entry in re.compile(r"(\s) vrf").split(data): + obj = self.render_config(self.generated_spec, entry) + if obj: + objs.append(obj) + + ansible_facts["ansible_network_resources"].pop("static_routes", None) + facts = {} + + 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) + entry_list = conf.split(" address-family") + config["address_families"] = [] + + if "router static" not in entry_list[0]: + config["vrf"] = entry_list[0].replace("!", "").strip() + + for item in entry_list[1:]: + routes = [] + address_family = {"routes": []} + address_family["afi"], address_family["safi"] = self.parse_af(item) + + destinations = re.findall(r"((?:\S+)/(?:\d+)) (?:.*)", item, re.M) + for dest in set(destinations): + route = {"next_hops": []} + route["dest"] = dest + + regex = r"%s .+$" % dest + cfg = re.findall(regex, item, re.M) + + for route_entry in cfg: + exit_point = {} + exit_point["forward_router_address"] = self.parse_faddr(route_entry) + exit_point["interface"] = self.parse_intf(route_entry) + exit_point["admin_distance"] = self.parse_admin_distance(route_entry) + + for x in [ + "tag", + "tunnel-id", + "metric", + "description", + "track", + "vrflabel", + "dest_vrf", + ]: + exit_point[x.replace("-", "_")] = self.parse_attrib( + route_entry, x.replace("dest_vrf", "vrf") + ) + + route["next_hops"].append(exit_point) + + routes.append(route) + address_family["routes"] = sorted(routes, key=lambda i: i["dest"]) + config["address_families"].append(address_family) + + return utils.remove_empties(config) + + def parse_af(self, item): + match = re.search(r"(?:\s*)(\w+)(?:\s*)(\w+)", item, re.M) + if match: + return match.group(1), match.group(2) + + def parse_faddr(self, item): + for x in item.split(" "): + if (":" in x or "." in x) and "/" not in x: + return x + + def parse_intf(self, item): + match = re.search(r" ((\w+)((?:\d)/(?:\d)/(?:\d)/(?:\d+)))", item) + if match: + return match.group(1) + + def parse_attrib(self, item, attrib): + match = re.search(r" %s (\S+)" % attrib, item) + if match: + val = match.group(1).strip("'") + if attrib in ["tunnel-id", "vrflabel", "tag", "metric"]: + val = int(val) + return val + + def parse_admin_distance(self, item): + split_item = item.split(" ") + for item in [ + "vrf", + "metric", + "tunnel-id", + "vrflabel", + "track", + "tag", + "description", + ]: + try: + del split_item[split_item.index(item) + 1] + del split_item[split_item.index(item)] + except ValueError: + continue + try: + return [ + i for i in split_item if "." not in i and ":" not in i and ord(i[0]) > 48 and ord(i[0]) < 57 + ][0] + except IndexError: + return None diff --git a/lib/ansible/modules/network/iosxr/iosxr_facts.py b/lib/ansible/modules/network/iosxr/iosxr_facts.py index 244cb617810..797d58dcd0f 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_facts.py +++ b/lib/ansible/modules/network/iosxr/iosxr_facts.py @@ -55,7 +55,7 @@ options: specific subset should not be collected. Valid subsets are 'all', 'lacp', 'lacp_interfaces', 'lldp_global', 'lldp_interfaces', 'interfaces', 'l2_interfaces', 'l3_interfaces', - 'lag_interfaces', 'acls'. + 'lag_interfaces', 'acls', 'acl_interfaces', 'static_routes. required: false version_added: "2.9" """ diff --git a/lib/ansible/modules/network/iosxr/iosxr_static_routes.py b/lib/ansible/modules/network/iosxr/iosxr_static_routes.py new file mode 100644 index 00000000000..4f75513fd5b --- /dev/null +++ b/lib/ansible/modules/network/iosxr/iosxr_static_routes.py @@ -0,0 +1,966 @@ +#!/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 iosxr_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: iosxr_static_routes +version_added: "2.10" +short_description: Manage static routes on devices running Cisco IOS-XR. +description: + - This module manages static routes on devices running Cisco IOS-XR. +author: Nilashish Chakraborty (@NilashishC) +options: + 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 + B(show running-config router static). + type: str + config: + description: A dictionary of static route options. + 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 + safi: + description: + - Specifies the subsequent address family indicator. + type: str + choices: ['unicast', 'multicast'] + required: True + routes: + description: A dictionary that specifies the static route configurations. + elements: dict + type: list + suboptions: + dest: + description: + - An IPv4 or IPv6 address in CIDR notation that specifies the destination network for the static route. + type: str + required: True + next_hops: + description: + - Next hops to the specified destination. + type: list + elements: dict + suboptions: + forward_router_address: + description: + - The IP address of the next hop that can be used to reach the destination network. + type: str + interface: + description: + - The interface to use to reach the destination. + type: str + dest_vrf: + description: + - The destination VRF. + type: str + admin_distance: + description: + - The administrative distance for this static route. + - Refer to vendor documentation for valid values. + type: int + metric: + description: + - Specifes the metric for this static route. + - Refer to vendor documentation for valid values. + type: int + description: + description: + - Specifies the description for this static route. + type: str + vrflabel: + description: + - Specifies the VRF label for this static route. + - Refer to vendor documentation for valid values. + type: int + tag: + description: + - Specifies a numeric tag for this static route. + - Refer to vendor documentation for valid values. + type: int + track: + description: + - Specifies the object to be tracked. + - This enables object tracking for static routes. + type: str + tunnel_id: + description: + - Specifies a tunnel id for the route. + - Refer to vendor documentation for valid values. + type: int + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ + +# Using merged + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#show running-config router static +# Sat Feb 22 07:46:30.089 UTC +# % No such configuration item(s) +# +- name: Merge the provided configuration with the exisiting running configuration + iosxr_static_routes: &merged + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + description: "LAB" + metric: 120 + tag: 10 + + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + + - dest: 192.0.2.32/28 + next_hops: + - forward_router_address: 192.0.2.11 + admin_distance: 100 + + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - interface: FastEthernet0/0/0/7 + description: "DC" + + - interface: FastEthernet0/0/0/8 + forward_router_address: 2001:db8:2000:2::1 + + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + description: "DEV" + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + track: ip_sla_2 + vrflabel: 124 + state: merged + +# After state +# ------------- +# RP/0/RP0/CPU0:ios#show running-config router static +# Sat Feb 22 07:49:11.754 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +# Using merged to update existing static routes + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#show running-config router static +# Sat Feb 22 07:49:11.754 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Update existing static routes configuration using merged + iosxr_static_routes: &merged_update + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + vrflabel: 2301 + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + description: "rt_test_1" + state: merged + +# After state +# ------------- +# RP/0/RP0/CPU0:ios#show running-config router static +# Sat Feb 22 07:49:11.754 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV vrflabel 2301 +# 192.0.2.80/28 vrf test_1 192.0.2.14 FastEthernet0/0/0/2 description rt_test_1 track ip_sla_2 vrflabel 124 +# ! +# ! +# ! + +# Using replaced to replace all next hop entries for a single destination network + +# Before state +# -------------- + +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Replace device configurations of static routes with provided configurations + iosxr_static_routes: &replaced + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV_NEW" + dest_vrf: dev_test_2 + state: replaced + +# After state +# ------------ +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 08:04:07.085 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf dev_test_2 FastEthernet0/0/0/3 192.0.2.15 description DEV_NEW +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +# Using overridden to override all static route entries on the device + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Overridde all static routes configuration with provided configuration + iosxr_static_routes: &overridden + config: + - vrf: DEV_NEW + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV1" + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:3000::/36 + next_hops: + - interface: FastEthernet0/0/0/4 + forward_router_address: 2001:db8:2000:2::2 + description: "PROD1" + track: ip_sla_1 + state: overridden + +# After state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 08:07:41.516 UTC +# router static +# vrf DEV_NEW +# address-family ipv4 unicast +# 192.0.2.48/28 FastEthernet0/0/0/3 192.0.2.15 description DEV1 +# ! +# address-family ipv6 unicast +# 2001:db8:3000::/36 FastEthernet0/0/0/4 2001:db8:2000:2::2 description PROD1 track ip_sla_1 +# ! +# ! +# ! + +# Using deleted to delete a single next hop for a destination network + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Delete a single next_hop from a destination network + iosxr_static_routes: + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + state: deleted + +# After state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +# Using deleted to delete a destination network entry + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Delete a destination network entry + iosxr_static_routes: + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + state: deleted + +# After state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +# Using deleted to delete all destination network entries under a single AFI + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Delete all destination network entries under a single AFI + iosxr_static_routes: + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + state: deleted + +# After state +# ------------ + +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 08:16:41.464 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# ! +# ! + +# Using deleted to remove all static route entries from the device + +# Before state +# ------------- +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 07:59:08.669 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.48/28 GigabitEthernet0/0/0/1 192.0.3.24 vrflabel 2302 +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Delete static routes configuration + iosxr_static_routes: &deleted + state: deleted + +# After state +# ------------ +# RP/0/RP0/CPU0:ios#sh running-config router static +# Sat Feb 22 08:50:43.038 UTC +# % No such configuration item(s) + +# Using gathered to gather static route facts from the device + +- name: Gather static routes facts from the device using iosxr_static_routes module + iosxr_static_routes: + state: gathered + +# Task output (redacted) +# ----------------------- +# "gathered": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "192.0.2.16/28", +# "next_hops": [ +# { +# "description": "LAB", +# "forward_router_address": "192.0.2.10", +# "interface": "FastEthernet0/0/0/1", +# "metric": 120, +# "tag": 10 +# }, +# { +# "interface": "FastEthernet0/0/0/5", +# "track": "ip_sla_1" +# } +# ] +# }, +# { +# "dest": "192.0.2.32/28", +# "next_hops": [ +# { +# "admin_distance": 100, +# "forward_router_address": "192.0.2.11" +# } +# ] +# } +# ], +# "safi": "unicast" +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2001:db8:1000::/36", +# "next_hops": [ +# { +# "description": "DC", +# "interface": "FastEthernet0/0/0/7" +# }, +# { +# "forward_router_address": "2001:db8:2000:2::1", +# "interface": "FastEthernet0/0/0/8" +# } +# ] +# } +# ], +# "safi": "unicast" +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "192.0.2.48/28", +# "next_hops": [ +# { +# "description": "DEV", +# "dest_vrf": "test_1", +# "forward_router_address": "192.0.2.12" +# }, +# { +# "forward_router_address": "192.0.3.24", +# "interface": "GigabitEthernet0/0/0/1", +# "vrflabel": 2302 +# } +# ] +# }, +# { +# "dest": "192.0.2.80/28", +# "next_hops": [ +# { +# "dest_vrf": "test_1", +# "forward_router_address": "192.0.2.14", +# "interface": "FastEthernet0/0/0/2", +# "track": "ip_sla_2", +# "vrflabel": 124 +# } +# ] +# } +# ], +# "safi": "unicast" +# } +# ], +# "vrf": "DEV_SITE" +# } +# ] + +# Using rendered + +- name: Render platform specific commands (without connecting to the device) + iosxr_static_routes: + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + description: "DEV" + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + track: ip_sla_2 + vrflabel: 124 + +# Task Output (redacted) +# ----------------------- +# "rendered": [ +# "router static"s, +# "vrf DEV_SITE", +# "address-family ipv4 unicast", +# "192.0.2.48/28 vrf test_1 192.0.2.12 description DEV", +# "192.0.2.80/28 vrf test_1 192.0.2.14 FastEthernet0/0/0/2 track ip_sla_2 vrflabel 124" + +# Using parsed + +# parsed.cfg +# ------------ +# Fri Nov 29 21:10:41.896 UTC +# router static +# address-family ipv4 unicast +# 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 +# 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 +# 192.0.2.32/28 192.0.2.11 100 +# ! +# address-family ipv6 unicast +# 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC +# 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 +# ! +# vrf DEV_SITE +# address-family ipv4 unicast +# 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV +# 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 +# ! +# ! +# ! + +- name: Use parsed state to convert externally supplied device specific static routes commands to structured format + iosxr_static_routes: + running_config: "{{ lookup('file', '../../fixtures/parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# "parsed": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "192.0.2.16/28", +# "next_hops": [ +# { +# "description": "LAB", +# "forward_router_address": "192.0.2.10", +# "interface": "FastEthernet0/0/0/1", +# "metric": 120, +# "tag": 10 +# }, +# { +# "interface": "FastEthernet0/0/0/5", +# "track": "ip_sla_1" +# } +# ] +# }, +# { +# "dest": "192.0.2.32/28", +# "next_hops": [ +# { +# "admin_distance": 100, +# "forward_router_address": "192.0.2.11" +# } +# ] +# } +# ], +# "safi": "unicast" +# }, +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2001:db8:1000::/36", +# "next_hops": [ +# { +# "description": "DC", +# "interface": "FastEthernet0/0/0/7" +# }, +# { +# "forward_router_address": "2001:db8:2000:2::1", +# "interface": "FastEthernet0/0/0/8" +# } +# ] +# } +# ], +# "safi": "unicast" +# } +# ] +# }, +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "192.0.2.48/28", +# "next_hops": [ +# { +# "description": "DEV", +# "dest_vrf": "test_1", +# "forward_router_address": "192.0.2.12" +# } +# ] +# }, +# { +# "dest": "192.0.2.80/28", +# "next_hops": [ +# { +# "dest_vrf": "test_1", +# "forward_router_address": "192.0.2.14", +# "interface": "FastEthernet0/0/0/2", +# "track": "ip_sla_2", +# "vrflabel": 124 +# } +# ] +# } +# ], +# "safi": "unicast" +# } +# ], +# "vrf": "DEV_SITE" +# } +# ] +# } +""" +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: + - router static + - vrf dev_site + - address-family ipv4 unicast + - 192.0.2.48/28 192.0.2.12 FastEthernet0/0/0/1 track ip_sla_10 description dev1 + - address-family ipv6 unicast + - no 2001:db8:1000::/36 + - 2001:db8:3000::/36 2001:db8:2000:2::2 FastEthernet0/0/0/4 track ip_sla_11 description prod1 +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.iosxr.argspec.static_routes.static_routes import Static_routesArgs +from ansible.module_utils.network.iosxr.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, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/test/integration/targets/iosxr_static_routes/defaults/main.yaml b/test/integration/targets/iosxr_static_routes/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/iosxr_static_routes/fixtures/parsed.cfg b/test/integration/targets/iosxr_static_routes/fixtures/parsed.cfg new file mode 100644 index 00000000000..3594833635b --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/fixtures/parsed.cfg @@ -0,0 +1,18 @@ +Fri Nov 29 21:10:41.896 UTC +router static + address-family ipv4 unicast + 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 + 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 + 192.0.2.32/28 192.0.2.11 100 + ! + address-family ipv6 unicast + 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC + 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 + ! + vrf DEV_SITE + address-family ipv4 unicast + 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV + 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 + ! + ! +! diff --git a/test/integration/targets/iosxr_static_routes/tasks/cli.yaml b/test/integration/targets/iosxr_static_routes/tasks/cli.yaml new file mode 100644 index 00000000000..337e34133b0 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- 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 }}" + delegate_to: localhost + +- name: Run test case (connection=network_cli) + include: "{{ test_case_to_run }}" + vars: + ansible_connection: network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_static_routes/tasks/main.yaml b/test/integration/targets/iosxr_static_routes/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/_populate_config.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/_populate_config.yaml new file mode 100644 index 00000000000..1c1d63b1d74 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/_populate_config.yaml @@ -0,0 +1,60 @@ +--- +- name: Setup + iosxr_static_routes: + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + description: "LAB" + metric: 120 + tag: 10 + + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + + - dest: 192.0.2.32/28 + next_hops: + - forward_router_address: 192.0.2.11 + admin_distance: 100 + + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - interface: FastEthernet0/0/0/7 + description: "DC" + + - interface: FastEthernet0/0/0/8 + forward_router_address: 2001:db8:2000:2::1 + + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + description: "DEV" + dest_vrf: test_1 + + - forward_router_address: 192.0.3.24 + interface: GigabitEthernet0/0/0/1 + vrflabel: 2302 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + track: ip_sla_2 + vrflabel: 124 + + state: merged + \ No newline at end of file diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/_remove_config.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/_remove_config.yaml new file mode 100644 index 00000000000..97d4d48ee2a --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/_remove_config.yaml @@ -0,0 +1,8 @@ +--- +- name: Remove Static Routes + cli_config: + config: "{{ lines }}" + vars: + lines: | + no router static + ignore_errors: yes diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/deleted.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/deleted.yaml new file mode 100644 index 00000000000..b6724c13aac --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/deleted.yaml @@ -0,0 +1,124 @@ +--- +- debug: + msg: "Start iosxr_static_routes deleted integration tests ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Delete a single next_hop from a destination network + iosxr_static_routes: &deleted_1 + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + state: deleted + register: result + + - assert: + that: + - '"router static" in result.commands' + - '"address-family ipv4 unicast" in result.commands' + - '"no 192.0.2.16/28 192.0.2.10 FastEthernet0/0/0/1" in result.commands' + - 'result.commands|length == 3' + + - name: Delete a single next_hop from a destination network (IDEMPOTENT) + iosxr_static_routes: *deleted_1 + register: result + + - assert: &unchanged + that: + - "result.changed == false" + - "result.commands|length == 0" + + - name: Delete a destination network entry + iosxr_static_routes: &deleted_2 + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + state: deleted + register: result + + - assert: + that: + - '"router static" in result.commands' + - '"vrf DEV_SITE" in result.commands' + - '"address-family ipv4 unicast" in result.commands' + - '"no 192.0.2.48/28" in result.commands' + - "result.commands|length == 4" + + - name: Delete a destination network entry (IDEMPOTENT) + iosxr_static_routes: *deleted_2 + register: result + + - assert: *unchanged + + - name: Delete all destination network entries under a single AFI + iosxr_static_routes: &deleted_3 + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + state: deleted + register: result + + - assert: + that: + - '"router static" in result.commands' + - '"vrf DEV_SITE" in result.commands' + - '"no address-family ipv4 unicast" in result.commands' + - "result.commands|length == 3" + + - name: Delete all destination network entries under a single AFI (IDEMPOTENT) + iosxr_static_routes: *deleted_3 + register: result + + - assert: *unchanged + + - include_tasks: _populate_config.yaml + + - name: Delete static routes configuration + iosxr_static_routes: &deleted + state: deleted + register: result + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ replaced['before'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that the correct set of commands were generated + assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that the after dicts were correctly generated + assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Delete all static routes (IDEMPOTENT) + iosxr_static_routes: *deleted + register: result + + - name: Assert that the previous task was idempotent + assert: *unchanged + + - name: Assert that the before dicts were correctly generated + assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/empty_config.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/empty_config.yaml new file mode 100644 index 00000000000..fd0625e0122 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/empty_config.yaml @@ -0,0 +1,47 @@ +--- +- debug: + msg: "START iosxr_static_routes empty_config integration tests on connection={{ ansible_connection }}" + +- name: Merged with empty config should give appropriate error message + iosxr_static_routes: + config: + state: merged + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state merged' + +- name: Replaced with empty config should give appropriate error message + iosxr_static_routes: + config: + state: replaced + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state replaced' + +- name: Overridden with empty config should give appropriate error message + iosxr_static_routes: + config: + state: overridden + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state overridden' + +- name: Parsed with empty running_config should give appropriate error message + iosxr_static_routes: + running_config: + state: parsed + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' \ No newline at end of file diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/gathered.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/gathered.yaml new file mode 100644 index 00000000000..2286b02ad39 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: "START iosxr_static_routes gathered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Gather static routes facts from the device using iosxr_static_routes module + iosxr_static_routes: + state: gathered + register: result + + - assert: + that: "{{ replaced['before'] | symmetric_difference(result['gathered']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml + diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/merged.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/merged.yaml new file mode 100644 index 00000000000..40eb02f45e7 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/merged.yaml @@ -0,0 +1,141 @@ +--- +- debug: + msg: "START iosxr_static_routes merged integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge the provided configuration with the exisiting running configuration + iosxr_static_routes: &merged + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + description: "LAB" + metric: 120 + tag: 10 + + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + + - dest: 192.0.2.32/28 + next_hops: + - forward_router_address: 192.0.2.11 + admin_distance: 100 + + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - interface: FastEthernet0/0/0/7 + description: "DC" + + - interface: FastEthernet0/0/0/8 + forward_router_address: 2001:db8:2000:2::1 + + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + description: "DEV" + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + track: ip_sla_2 + vrflabel: 124 + state: merged + register: result + + - name: Assert that before dicts were correctly generated + assert: + that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - set_fact: + diff: "{{ merged['after'] | symmetric_difference(result['after']) }}" + + - name: Assert that after dicts was correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + iosxr_static_routes: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dicts were correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Update existing configuration using merged + iosxr_static_routes: &merged_update + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + vrflabel: 2301 + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + description: "rt_test_1" + register: result + + - name: Assert that before dicts were correctly generated + assert: + that: "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ merged['update_commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that after dicts were correctly generated + assert: + that: "{{ merged['update_after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Update existing static_routes configuration using merged (IDEMPOTENT) + iosxr_static_routes: *merged_update + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/overridden.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/overridden.yaml new file mode 100644 index 00000000000..44f62efe44d --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/overridden.yaml @@ -0,0 +1,66 @@ +--- +- debug: + msg: "START iosxr_static_routes overridden integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Overridde all static routes configuration with provided configuration + iosxr_static_routes: &overridden + config: + - vrf: DEV_NEW + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV1" + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:3000::/36 + next_hops: + - interface: FastEthernet0/0/0/4 + forward_router_address: 2001:db8:2000:2::2 + description: "PROD1" + track: ip_sla_1 + state: overridden + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ replaced['before'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Overridde all static routes configuration with given configuration (IDEMPOTENT) + iosxr_static_routes: *overridden + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dict is correctly generated + assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/parsed.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/parsed.yaml new file mode 100644 index 00000000000..0932818c519 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/parsed.yaml @@ -0,0 +1,15 @@ +--- +- debug: + msg: "START iosxr_static_routes parsed integration tests on connection={{ ansible_connection }}" + +- block: + - name: Use parsed state to convert externally supplied device specific static routes commands to structured format + iosxr_static_routes: + running_config: "{{ lookup('file', '../../fixtures/parsed.cfg') }}" + state: parsed + register: result + + - assert: + that: "{{ merged['after'] | symmetric_difference(result['parsed']) |length==0 }}" + + \ No newline at end of file diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/rendered.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/rendered.yaml new file mode 100644 index 00000000000..f469eb1b2cf --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/rendered.yaml @@ -0,0 +1,76 @@ +--- +- debug: + msg: "START iosxr_static_routes rendered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + - name: Use rendered state to convert task input to device specific commands + iosxr_static_routes: + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.12 + description: "DEV" + dest_vrf: test_1 + + - dest: 192.0.2.80/28 + next_hops: + - interface: FastEthernet0/0/0/2 + forward_router_address: 192.0.2.14 + dest_vrf: test_1 + track: ip_sla_2 + vrflabel: 124 + + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + description: "LAB" + metric: 120 + tag: 10 + + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + + - dest: 192.0.2.32/28 + next_hops: + - forward_router_address: 192.0.2.11 + admin_distance: 100 + + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - interface: FastEthernet0/0/0/7 + description: "DC" + + - interface: FastEthernet0/0/0/8 + forward_router_address: 2001:db8:2000:2::1 + state: rendered + register: result + + - assert: + that: "{{ merged['commands'] | symmetric_difference(result['rendered']) |length==0 }}" + + - name: Gather static routes facts from the device and assert that its empty + iosxr_static_routes: + state: gathered + register: result + + - name: Make sure that rendered task actually did not make any changes to the device + assert: + that: "{{ result['gathered'] == [] }}" + + always: + - include_tasks: _remove_config.yaml \ No newline at end of file diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/replaced.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/replaced.yaml new file mode 100644 index 00000000000..b0f9cf84294 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/replaced.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: "START iosxr_static_routes replaced integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Replace device configurations of static routes with provided configurations + iosxr_static_routes: &replaced + config: + - vrf: DEV_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV_NEW" + dest_vrf: dev_test_2 + state: replaced + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ replaced['before'] | symmetric_difference(result['before']) |length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Replace device configurations of listed vrfs/global entry with provided configuration (IDEMPOTENT) + iosxr_static_routes: *replaced + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + - "result.commands|length == 0" + + - name: Assert that before dict is correctly generated + assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_static_routes/tests/cli/rtt.yaml b/test/integration/targets/iosxr_static_routes/tests/cli/rtt.yaml new file mode 100644 index 00000000000..ccc63bcbd5f --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/tests/cli/rtt.yaml @@ -0,0 +1,73 @@ +--- +- debug: + msg: "START iosxr_static_routes round trip integration tests on connection={{ ansible_connection }}" + +- block: + - include_tasks: _remove_config.yaml + + - name: Apply the provided configuration (base config) + iosxr_static_routes: + config: + - address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + admin_distance: 105 + track: ip_sla_2 + - vrf: DEV_SITE + address_families: + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:3000::/36 + next_hops: + - forward_router_address: 2001:db8:2000:2::2 + interface: FastEthernet0/0/0/11 + description: PROD1 + state: merged + register: base_config + + - name: Gather interfaces facts + iosxr_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: + - static_routes + + - name: Apply the provided configuration (config to be reverted) + iosxr_static_routes: + config: + - vrf: TEST_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.80/28 + next_hops: + - forward_router_address: 192.0.2.12 + interface: FastEthernet0/0/0/3 + description: "DEV_MOVED" + dest_vrf: dev_moved + state: overridden + register: result + + - name: Assert that changes were applied + assert: + that: "{{ round_trip['after'] | symmetric_difference(result['after']) |length == 0 }}" + + - name: Revert back to base config using facts round trip + iosxr_static_routes: + config: "{{ ansible_facts['network_resources']['static_routes'] }}" + state: overridden + register: revert + + - name: Assert that config was reverted + assert: + that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/iosxr_static_routes/vars/main.yaml b/test/integration/targets/iosxr_static_routes/vars/main.yaml new file mode 100644 index 00000000000..68881b51d38 --- /dev/null +++ b/test/integration/targets/iosxr_static_routes/vars/main.yaml @@ -0,0 +1,281 @@ +--- +merged: + before: [] + + commands: + - router static + - address-family ipv4 unicast + - 192.0.2.16/28 192.0.2.10 FastEthernet0/0/0/1 description LAB metric 120 tag 10 + - 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 + - 192.0.2.32/28 192.0.2.11 100 + - address-family ipv6 unicast + - 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC + - 2001:db8:1000::/36 2001:db8:2000:2::1 FastEthernet0/0/0/8 + - vrf DEV_SITE + - address-family ipv4 unicast + - 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV + - 192.0.2.80/28 vrf test_1 192.0.2.14 FastEthernet0/0/0/2 track ip_sla_2 vrflabel 124 + + update_commands: + - router static + - vrf DEV_SITE + - address-family ipv4 unicast + - 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV vrflabel 2301 + - 192.0.2.80/28 vrf test_1 192.0.2.14 FastEthernet0/0/0/2 description rt_test_1 track ip_sla_2 vrflabel 124 + + after: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - description: LAB + forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + metric: 120 + tag: 10 + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + - dest: 192.0.2.32/28 + next_hops: + - admin_distance: 100 + forward_router_address: 192.0.2.11 + safi: unicast + - afi: ipv6 + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - description: DC + interface: FastEthernet0/0/0/7 + - forward_router_address: 2001:db8:2000:2::1 + interface: FastEthernet0/0/0/8 + safi: unicast + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.48/28 + next_hops: + - description: DEV + dest_vrf: test_1 + forward_router_address: 192.0.2.12 + - dest: 192.0.2.80/28 + next_hops: + - dest_vrf: test_1 + forward_router_address: 192.0.2.14 + interface: FastEthernet0/0/0/2 + track: ip_sla_2 + vrflabel: 124 + safi: unicast + vrf: DEV_SITE + + update_after: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - description: LAB + forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + metric: 120 + tag: 10 + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + - dest: 192.0.2.32/28 + next_hops: + - admin_distance: 100 + forward_router_address: 192.0.2.11 + safi: unicast + - afi: ipv6 + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - description: DC + interface: FastEthernet0/0/0/7 + - forward_router_address: 2001:db8:2000:2::1 + interface: FastEthernet0/0/0/8 + safi: unicast + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.48/28 + next_hops: + - description: DEV + dest_vrf: test_1 + forward_router_address: 192.0.2.12 + vrflabel: 2301 + - dest: 192.0.2.80/28 + next_hops: + - dest_vrf: test_1 + forward_router_address: 192.0.2.14 + interface: FastEthernet0/0/0/2 + track: ip_sla_2 + vrflabel: 124 + description: rt_test_1 + safi: unicast + vrf: DEV_SITE + + +replaced: + before: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - description: LAB + forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + metric: 120 + tag: 10 + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + - dest: 192.0.2.32/28 + next_hops: + - admin_distance: 100 + forward_router_address: 192.0.2.11 + safi: unicast + - afi: ipv6 + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - description: DC + interface: FastEthernet0/0/0/7 + - forward_router_address: 2001:db8:2000:2::1 + interface: FastEthernet0/0/0/8 + safi: unicast + + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.48/28 + next_hops: + - description: DEV + dest_vrf: test_1 + forward_router_address: 192.0.2.12 + + - forward_router_address: 192.0.3.24 + interface: GigabitEthernet0/0/0/1 + vrflabel: 2302 + + - dest: 192.0.2.80/28 + next_hops: + - dest_vrf: test_1 + forward_router_address: 192.0.2.14 + interface: FastEthernet0/0/0/2 + track: ip_sla_2 + vrflabel: 124 + safi: unicast + vrf: DEV_SITE + + commands: + - router static + - vrf DEV_SITE + - address-family ipv4 unicast + - no 192.0.2.48/28 192.0.3.24 GigabitEthernet0/0/0/1 + - no 192.0.2.48/28 vrf test_1 192.0.2.12 + - 192.0.2.48/28 vrf dev_test_2 192.0.2.15 FastEthernet0/0/0/3 description DEV_NEW + + after: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - description: LAB + forward_router_address: 192.0.2.10 + interface: FastEthernet0/0/0/1 + metric: 120 + tag: 10 + - interface: FastEthernet0/0/0/5 + track: ip_sla_1 + - dest: 192.0.2.32/28 + next_hops: + - admin_distance: 100 + forward_router_address: 192.0.2.11 + safi: unicast + - afi: ipv6 + routes: + - dest: 2001:db8:1000::/36 + next_hops: + - description: DC + interface: FastEthernet0/0/0/7 + - forward_router_address: 2001:db8:2000:2::1 + interface: FastEthernet0/0/0/8 + safi: unicast + + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV_NEW" + dest_vrf: dev_test_2 + + - dest: 192.0.2.80/28 + next_hops: + - dest_vrf: test_1 + forward_router_address: 192.0.2.14 + interface: FastEthernet0/0/0/2 + track: ip_sla_2 + vrflabel: 124 + safi: unicast + vrf: DEV_SITE + + +overridden: + commands: + - router static + - no vrf DEV_SITE + - no address-family ipv4 unicast + - no address-family ipv6 unicast + - vrf DEV_NEW + - address-family ipv4 unicast + - 192.0.2.48/28 192.0.2.15 FastEthernet0/0/0/3 description DEV1 + - address-family ipv6 unicast + - 2001:db8:3000::/36 2001:db8:2000:2::2 FastEthernet0/0/0/4 description PROD1 track ip_sla_1 + + after: + - vrf: DEV_NEW + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.15 + interface: FastEthernet0/0/0/3 + description: "DEV1" + + - afi: ipv6 + safi: unicast + routes: + - dest: 2001:db8:3000::/36 + next_hops: + - interface: FastEthernet0/0/0/4 + forward_router_address: 2001:db8:2000:2::2 + description: "PROD1" + track: ip_sla_1 + +deleted: + commands: + - no router static + + after: [] + +round_trip: + after: + - vrf: TEST_SITE + address_families: + - afi: ipv4 + safi: unicast + routes: + - dest: 192.0.2.80/28 + next_hops: + - forward_router_address: 192.0.2.12 + interface: FastEthernet0/0/0/3 + description: "DEV_MOVED" + dest_vrf: dev_moved + \ No newline at end of file diff --git a/test/units/modules/network/iosxr/fixtures/iosxr_static_routes_config.cfg b/test/units/modules/network/iosxr/fixtures/iosxr_static_routes_config.cfg new file mode 100644 index 00000000000..36c9eb2330c --- /dev/null +++ b/test/units/modules/network/iosxr/fixtures/iosxr_static_routes_config.cfg @@ -0,0 +1,18 @@ +Fri Nov 29 21:10:41.896 UTC +router static + address-family ipv4 unicast + 192.0.2.16/28 FastEthernet0/0/0/1 192.0.2.10 tag 10 description LAB metric 120 + 192.0.2.16/28 FastEthernet0/0/0/5 track ip_sla_1 + 192.0.2.32/28 192.0.2.11 100 + ! + address-family ipv6 unicast + 2001:db8:1000::/36 FastEthernet0/0/0/7 description DC + 2001:db8:1000::/36 FastEthernet0/0/0/8 2001:db8:2000:2::1 + ! + vrf DEV_SITE + address-family ipv4 unicast + 192.0.2.48/28 vrf test_1 192.0.2.12 description DEV + 192.0.2.80/28 vrf test_1 FastEthernet0/0/0/2 192.0.2.14 vrflabel 124 track ip_sla_2 + ! + ! +! \ No newline at end of file diff --git a/test/units/modules/network/iosxr/test_iosxr_static_routes.py b/test/units/modules/network/iosxr/test_iosxr_static_routes.py new file mode 100644 index 00000000000..257b855ba67 --- /dev/null +++ b/test/units/modules/network/iosxr/test_iosxr_static_routes.py @@ -0,0 +1,395 @@ +# +# (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.iosxr import iosxr_static_routes +from units.modules.utils import set_module_args +from .iosxr_module import TestIosxrModule, load_fixture + + +class TestIosxrStaticRoutesModule(TestIosxrModule): + module = iosxr_static_routes + + def setUp(self): + super(TestIosxrStaticRoutesModule, 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_execute_show_command = patch( + "ansible.module_utils.network.iosxr.facts.static_routes.static_routes.Static_routesFacts.get_device_data" + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosxrStaticRoutesModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None): + def load_from_file(*args, **kwargs): + return load_fixture("iosxr_static_routes_config.cfg") + + self.execute_show_command.side_effect = load_from_file + + def test_iosxr_static_routes_merged(self): + set_module_args( + dict( + config=[ + dict( + vrf="dev_site", + address_families=[ + dict( + afi="ipv6", + safi="unicast", + routes=[ + dict( + dest="1200:10::/64", + next_hops=[ + dict( + interface="GigabitEthernet0/0/0/1", + admin_distance=55, + ) + ], + ) + ], + ) + ], + ) + ], + state="merged", + ) + ) + commands = [ + "router static", + "vrf dev_site", + "address-family ipv6 unicast", + "1200:10::/64 GigabitEthernet0/0/0/1 55", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_merged_idempotent(self): + set_module_args( + dict( + config=[ + dict( + vrf="DEV_SITE", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.2.48/28", + next_hops=[ + dict( + interface="192.0.2.12", + description="DEV", + dest_vrf="test_1", + ) + ], + ) + ], + ) + ], + ) + ], + state="merged", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_static_routes_default(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.168.1.0/24", + next_hops=[ + dict( + interface="GigabitEthernet0/0/0/2", + track="ip_sla_2", + vrflabel=1200, + ) + ], + ) + ], + ) + ] + ) + ] + ) + ) + commands = [ + "router static", + "address-family ipv4 unicast", + "192.168.1.0/24 GigabitEthernet0/0/0/2 track ip_sla_2 vrflabel 1200", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_default_idempotent(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.2.32/28", + next_hops=[ + dict( + forward_router_address="192.0.2.11", + admin_distance=100, + ) + ], + ) + ], + ) + ] + ) + ] + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_static_routes_replaced(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.2.16/28", + next_hops=[ + dict( + forward_router_address="192.0.2.11", + admin_distance=100, + ) + ], + ) + ], + ) + ] + ) + ], + state="replaced", + ) + ) + commands = [ + "router static", + "address-family ipv4 unicast", + "no 192.0.2.16/28 192.0.2.10 FastEthernet0/0/0/1", + "no 192.0.2.16/28 FastEthernet0/0/0/5", + "192.0.2.16/28 192.0.2.11 100", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_replaced_idempotent(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.2.16/28", + next_hops=[ + dict( + interface="FastEthernet0/0/0/5", + track="ip_sla_1", + ), + dict( + interface="FastEthernet0/0/0/1", + forward_router_address="192.0.2.10", + tag=10, + description="LAB", + metric=120, + ), + ], + ) + ], + ) + ] + ) + ], + state="replaced", + ) + ) + self.execute_module(changed=False, commands=[]) + + def test_iosxr_static_routes_overridden(self): + set_module_args( + dict( + config=[ + dict( + vrf="DEV_SITE_NEW", + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.4.16/28", + next_hops=[ + dict( + interface="FastEthernet0/0/0/5", + track="ip_sla_1", + ), + dict( + interface="FastEthernet0/0/0/1", + forward_router_address="192.0.2.10", + tag=10, + description="LAB", + metric=120, + ), + ], + ) + ], + ) + ], + ) + ], + state="overridden", + ) + ) + commands = [ + "router static", + "no address-family ipv4 unicast", + "no address-family ipv6 unicast", + "no vrf DEV_SITE", + "vrf DEV_SITE_NEW", + "address-family ipv4 unicast", + "192.0.4.16/28 192.0.2.10 FastEthernet0/0/0/1 description LAB metric 120 tag 10", + "192.0.4.16/28 FastEthernet0/0/0/5 track ip_sla_1", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_deleted_next_hop(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[ + dict( + dest="192.0.2.16/28", + next_hops=[ + dict(interface="FastEthernet0/0/0/5") + ], + ) + ], + ) + ] + ) + ], + state="deleted", + ) + ) + + commands = [ + "router static", + "address-family ipv4 unicast", + "no 192.0.2.16/28 FastEthernet0/0/0/5", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_deleted_dest(self): + set_module_args( + dict( + config=[ + dict( + address_families=[ + dict( + afi="ipv4", + safi="unicast", + routes=[dict(dest="192.0.2.16/28")], + ) + ] + ) + ], + state="deleted", + ) + ) + + commands = ["router static", "address-family ipv4 unicast", "no 192.0.2.16/28"] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_deleted_afi(self): + set_module_args( + dict( + config=[dict(address_families=[dict(afi="ipv4", safi="unicast")])], + state="deleted", + ) + ) + + commands = [ + "router static", + "no address-family ipv4 unicast", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_deleted_vrf(self): + set_module_args(dict(config=[dict(vrf="DEV_SITE")], state="deleted")) + + commands = [ + "router static", + "no vrf DEV_SITE", + ] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_static_routes_deleted_all(self): + set_module_args(dict(state="deleted")) + + commands = [ + "no router static", + ] + self.execute_module(changed=True, commands=commands)