diff --git a/lib/ansible/module_utils/network/ios/argspec/static_routes/__init__.py b/lib/ansible/module_utils/network/ios/argspec/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/argspec/static_routes/static_routes.py b/lib/ansible/module_utils/network/ios/argspec/static_routes/static_routes.py new file mode 100644 index 00000000000..33621879873 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/argspec/static_routes/static_routes.py @@ -0,0 +1,85 @@ +# +# -*- 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 ios_static_routes module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Static_RoutesArgs(object): + """The arg spec for the ios_static_routes module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'vrf': {'type': 'str'}, + 'address_families': { + 'elements': 'dict', + 'type': 'list', + 'options': { + 'afi': {'required': True, 'choices': ['ipv4', 'ipv6'], 'type': 'str'}, + 'routes': { + 'elements': 'dict', + 'type': 'list', + 'options': { + 'dest': {'required': True, 'type': 'str'}, + 'topology': {'type': 'str'}, + 'next_hops': { + 'elements': 'dict', + 'type': 'list', + 'options': { + 'forward_router_address': {'type': 'str'}, + 'interface': {'type': 'str'}, + 'dhcp': {'type': 'bool'}, + 'distance_metric': {'type': 'int'}, + 'global': {'type': 'bool'}, + 'name': {'type': 'str'}, + 'multicast': {'type': 'bool'}, + 'permanent': {'type': 'bool'}, + 'tag': {'type': 'int'}, + 'track': {'type': 'int'} + } + } + } + } + } + } + }, + 'type': 'list' + }, + 'running_config': {'type': 'str'}, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted', 'gathered', 'rendered', 'parsed'], + 'default': 'merged', + 'type': 'str' + } + } diff --git a/lib/ansible/module_utils/network/ios/config/static_routes/__init__.py b/lib/ansible/module_utils/network/ios/config/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/config/static_routes/static_routes.py b/lib/ansible/module_utils/network/ios/config/static_routes/static_routes.py new file mode 100644 index 00000000000..0e3eb4a66be --- /dev/null +++ b/lib/ansible/module_utils/network/ios/config/static_routes/static_routes.py @@ -0,0 +1,532 @@ +# +# -*- 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 ios_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 copy +from ansible.module_utils.six import iteritems +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.ios.facts.facts import Facts +from ansible.module_utils.network.ios.utils.utils import new_dict_to_set, validate_n_expand_ipv4, filter_dict_having_none_value + + +class Static_Routes(ConfigBase): + """ + The ios_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} + commands = list() + warnings = 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) + 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 + """ + 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'] + 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)) + commands = [] + if state == 'overridden': + commands = self._state_overridden(want, have) + elif state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged' or state == 'rendered': + commands = self._state_merged(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + 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 = [] + + # Drill each iteration of want n have and then based on dest and afi tyoe comparison take config call + for w in want: + for addr_want in w.get('address_families'): + for route_want in addr_want.get('routes'): + check = False + for h in have: + if h.get('address_families'): + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + if route_want.get('dest') == route_have.get('dest')\ + and addr_want['afi'] == addr_have['afi']: + check = True + have_set = set() + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + new_dict_to_set(addr_have, [], have_set, 0) + # Check if the have dict next_hops value is diff from want dict next_hops + have_dict = filter_dict_having_none_value(route_want.get('next_hops')[0], + route_have.get('next_hops')[0]) + # update the have_dict with forward_router_address + have_dict.update({'forward_router_address': route_have.get('next_hops')[0]. + get('forward_router_address')}) + # updating the have_dict with next_hops val that's not None + new_have_dict = {} + for k, v in have_dict.items(): + if v is not None: + new_have_dict.update({k: v}) + + # Set the new config from the user provided want config + cmd = self._set_config(w, h, addr_want, route_want, route_have, new_hops, have_set) + + if cmd: + # since inplace update isn't allowed for static routes, preconfigured + # static routes needs to be deleted before the new want static routes changes + # are applied + clear_route_have = copy.deepcopy(route_have) + # inplace update is allowed in case of ipv6 static routes, so not deleting it + # before applying the want changes + if ':' not in route_want.get('dest'): + commands.extend(self._clear_config({}, h, {}, addr_have, + {}, clear_route_have)) + commands.extend(cmd) + if check: + break + if check: + break + if not check: + # For configuring any non-existing want config + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + commands.extend(self._set_config(w, {}, addr_want, route_want, {}, new_hops, set())) + commands = [each for each in commands if 'no' in each] + \ + [each for each in commands if 'no' not in each] + + 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 = [] + # Creating a copy of want, so that want dict is intact even after delete operation + # performed during override want n have comparison + temp_want = copy.deepcopy(want) + + # Drill each iteration of want n have and then based on dest and afi tyoe comparison take config call + for h in have: + if h.get('address_families'): + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + check = False + for w in temp_want: + for addr_want in w.get('address_families'): + count = 0 + for route_want in addr_want.get('routes'): + if route_want.get('dest') == route_have.get('dest') \ + and addr_want['afi'] == addr_have['afi']: + check = True + have_set = set() + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + new_dict_to_set(addr_have, [], have_set, 0) + commands.extend(self._clear_config(w, h, addr_want, addr_have, + route_want, route_have)) + commands.extend(self._set_config(w, h, addr_want, + route_want, route_have, new_hops, have_set)) + del addr_want.get('routes')[count] + count += 1 + if check: + break + if check: + break + if not check: + commands.extend(self._clear_config({}, h, {}, addr_have, {}, route_have)) + # For configuring any non-existing want config + for w in temp_want: + for addr_want in w.get('address_families'): + for route_want in addr_want.get('routes'): + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + commands.extend(self._set_config(w, {}, addr_want, route_want, {}, new_hops, set())) + # Arranging the cmds suct that all delete cmds are fired before all set cmds + commands = [each for each in sorted(commands) if 'no' in each] + \ + [each for each in sorted(commands) if 'no' not in each] + + 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 = [] + + # Drill each iteration of want n have and then based on dest and afi tyoe comparison take config call + for w in want: + for addr_want in w.get('address_families'): + for route_want in addr_want.get('routes'): + check = False + for h in have: + if h.get('address_families'): + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + if route_want.get('dest') == route_have.get('dest')\ + and addr_want['afi'] == addr_have['afi']: + check = True + have_set = set() + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + new_dict_to_set(addr_have, [], have_set, 0) + commands.extend(self._set_config(w, h, addr_want, + route_want, route_have, new_hops, have_set)) + if check: + break + if check: + break + if not check: + # For configuring any non-existing want config + new_hops = [] + for each in route_want.get('next_hops'): + want_set = set() + new_dict_to_set(each, [], want_set, 0) + new_hops.append(want_set) + commands.extend(self._set_config(w, {}, addr_want, route_want, {}, new_hops, set())) + + 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 want: + # Drill each iteration of want n have and then based on dest and afi type comparison fire delete config call + for w in want: + if w.get('address_families'): + for addr_want in w.get('address_families'): + for route_want in addr_want.get('routes'): + check = False + for h in have: + if h.get('address_families'): + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + if route_want.get('dest') == route_have.get('dest') \ + and addr_want['afi'] == addr_have['afi']: + check = True + if route_want.get('next_hops'): + commands.extend(self._clear_config({}, w, {}, addr_want, {}, route_want)) + else: + commands.extend(self._clear_config({}, h, {}, addr_have, {}, route_have)) + if check: + break + if check: + break + else: + for h in have: + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + if w.get('vrf') == h.get('vrf'): + commands.extend(self._clear_config({}, h, {}, addr_have, {}, route_have)) + else: + # Drill each iteration of have and then based on dest and afi type comparison fire delete config call + for h in have: + for addr_have in h.get('address_families'): + for route_have in addr_have.get('routes'): + commands.extend(self._clear_config({}, h, {}, addr_have, {}, route_have)) + + return commands + + def prepare_config_commands(self, config_dict, cmd): + """ + function to parse the input dict and form the prepare the config commands + :rtype: A str + :returns: The command necessary to configure the static routes + """ + + dhcp = config_dict.get('dhcp') + distance_metric = config_dict.get('distance_metric') + forward_router_address = config_dict.get('forward_router_address') + global_route_config = config_dict.get('global') + interface = config_dict.get('interface') + multicast = config_dict.get('multicast') + name = config_dict.get('name') + permanent = config_dict.get('permanent') + tag = config_dict.get('tag') + track = config_dict.get('track') + dest = config_dict.get('dest') + temp_dest = dest.split('/') + if temp_dest and ':' not in dest: + dest = validate_n_expand_ipv4(self._module, {'address': dest}) + + cmd = cmd + dest + if interface: + cmd = cmd + ' {0}'.format(interface) + if forward_router_address: + cmd = cmd + ' {0}'.format(forward_router_address) + if dhcp: + cmd = cmd + ' DHCP' + if distance_metric: + cmd = cmd + ' {0}'.format(distance_metric) + if global_route_config: + cmd = cmd + ' global' + if multicast: + cmd = cmd + ' multicast' + if name: + cmd = cmd + ' name {0}'.format(name) + if permanent: + cmd = cmd + ' permanent' + elif track: + cmd = cmd + ' track {0}'.format(track) + if tag: + cmd = cmd + ' tag {0}'.format(tag) + + return cmd + + def _set_config(self, want, have, addr_want, route_want, route_have, hops, have_set): + """ + Set the interface config based on the want and have config + :rtype: A list + :returns: The commands necessary to configure the static routes + """ + + commands = [] + cmd = None + + vrf_diff = False + topology_diff = False + want_vrf = want.get('vrf') + have_vrf = have.get('vrf') + if want_vrf != have_vrf: + vrf_diff = True + want_topology = want.get('topology') + have_topology = have.get('topology') + if want_topology != have_topology: + topology_diff = True + + have_dest = route_have.get('dest') + if have_dest: + have_set.add(tuple(iteritems({'dest': have_dest}))) + + # configure set cmd for each hops under the same destination + for each in hops: + diff = each - have_set + if vrf_diff: + each.add(tuple(iteritems({'vrf': want_vrf}))) + if topology_diff: + each.add(tuple(iteritems({'topology': want_topology}))) + if diff or vrf_diff or topology_diff: + if want_vrf and not vrf_diff: + each.add(tuple(iteritems({'vrf': want_vrf}))) + if want_topology and not vrf_diff: + each.add(tuple(iteritems({'topology': want_topology}))) + each.add(tuple(iteritems({'afi': addr_want.get('afi')}))) + each.add(tuple(iteritems({'dest': route_want.get('dest')}))) + temp_want = {} + for each_want in each: + temp_want.update(dict(each_want)) + + if temp_want.get('afi') == 'ipv4': + cmd = 'ip route ' + vrf = temp_want.get('vrf') + if vrf: + cmd = cmd + 'vrf {0} '.format(vrf) + cmd = self.prepare_config_commands(temp_want, cmd) + elif temp_want.get('afi') == 'ipv6': + cmd = 'ipv6 route ' + cmd = self.prepare_config_commands(temp_want, cmd) + commands.append(cmd) + + return commands + + def _clear_config(self, want, have, addr_want, addr_have, route_want, route_have): + """ + Delete the interface config based on the want and have config + :rtype: A list + :returns: The commands necessary to configure the static routes + """ + + commands = [] + cmd = None + + vrf_diff = False + topology_diff = False + want_vrf = want.get('vrf') + have_vrf = have.get('vrf') + if want_vrf != have_vrf: + vrf_diff = True + want_topology = want.get('topology') + have_topology = have.get('topology') + if want_topology != have_topology: + topology_diff = True + + want_set = set() + new_dict_to_set(addr_want, [], want_set, 0) + + have_hops = [] + for each in route_have.get('next_hops'): + temp_have_set = set() + new_dict_to_set(each, [], temp_have_set, 0) + have_hops.append(temp_have_set) + + # configure delete cmd for each hops under the same destination + for each in have_hops: + diff = each - want_set + if vrf_diff: + each.add(tuple(iteritems({'vrf': have_vrf}))) + if topology_diff: + each.add(tuple(iteritems({'topology': want_topology}))) + if diff or vrf_diff or topology_diff: + if want_vrf and not vrf_diff: + each.add(tuple(iteritems({'vrf': want_vrf}))) + if want_topology and not vrf_diff: + each.add(tuple(iteritems({'topology': want_topology}))) + if addr_want: + each.add(tuple(iteritems({'afi': addr_want.get('afi')}))) + else: + each.add(tuple(iteritems({'afi': addr_have.get('afi')}))) + if route_want: + each.add(tuple(iteritems({'dest': route_want.get('dest')}))) + else: + each.add(tuple(iteritems({'dest': route_have.get('dest')}))) + temp_want = {} + for each_want in each: + temp_want.update(dict(each_want)) + + if temp_want.get('afi') == 'ipv4': + cmd = 'no ip route ' + vrf = temp_want.get('vrf') + if vrf: + cmd = cmd + 'vrf {0} '.format(vrf) + cmd = self.prepare_config_commands(temp_want, cmd) + elif temp_want.get('afi') == 'ipv6': + cmd = 'no ipv6 route ' + cmd = self.prepare_config_commands(temp_want, cmd) + commands.append(cmd) + + return commands diff --git a/lib/ansible/module_utils/network/ios/facts/facts.py b/lib/ansible/module_utils/network/ios/facts/facts.py index 6f0163f98e2..2a2f539fa12 100644 --- a/lib/ansible/module_utils/network/ios/facts/facts.py +++ b/lib/ansible/module_utils/network/ios/facts/facts.py @@ -24,6 +24,7 @@ from ansible.module_utils.network.ios.facts.lldp_global.lldp_global import Lldp_ from ansible.module_utils.network.ios.facts.lldp_interfaces.lldp_interfaces import Lldp_InterfacesFacts from ansible.module_utils.network.ios.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts from ansible.module_utils.network.ios.facts.acl_interfaces.acl_interfaces import Acl_InterfacesFacts +from ansible.module_utils.network.ios.facts.static_routes.static_routes import Static_RoutesFacts from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config @@ -45,6 +46,7 @@ FACT_RESOURCE_SUBSETS = dict( lldp_interfaces=Lldp_InterfacesFacts, l3_interfaces=L3_InterfacesFacts, acl_interfaces=Acl_InterfacesFacts, + static_routes=Static_RoutesFacts, ) diff --git a/lib/ansible/module_utils/network/ios/facts/static_routes/__init__.py b/lib/ansible/module_utils/network/ios/facts/static_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/facts/static_routes/static_routes.py b/lib/ansible/module_utils/network/ios/facts/static_routes/static_routes.py new file mode 100644 index 00000000000..f7ada0b3f50 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/facts/static_routes/static_routes.py @@ -0,0 +1,225 @@ +# +# -*- 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 ios_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 + + +from copy import deepcopy +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.ios.utils.utils import netmask_to_cidr +from ansible.module_utils.network.ios.argspec.static_routes.static_routes import Static_RoutesArgs + + +class Static_RoutesFacts(object): + """ The ios_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_static_routes_data(self, connection): + return connection.get('sh running-config | include ip route|ipv6 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 + """ + + objs = [] + if not data: + data = self.get_static_routes_data(connection) + # operate on a collection of resource x + config = data.split('\n') + + same_dest = self.populate_destination(config) + for key in same_dest.keys(): + if key: + obj = self.render_config(self.generated_spec, key, same_dest[key]) + if obj: + objs.append(obj) + facts = {} + + # append all static routes address_family with NO VRF together + no_vrf_address_family = { + 'address_families': [each.get('address_families')[0] for each in objs if each.get('vrf') is None] + } + + temp_objs = [each for each in objs if each.get('vrf') is not None] + temp_objs.append(no_vrf_address_family) + objs = temp_objs + + 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 update_netmask_to_cidr(self, filter, pos, del_pos): + netmask = filter.split(' ') + dest = netmask[pos] + '/' + netmask_to_cidr(netmask[del_pos]) + netmask[pos] = dest + del netmask[del_pos] + filter_vrf = ' ' + return filter_vrf.join(netmask), dest + + def populate_destination(self, config): + same_dest = {} + ip_str = '' + for i in sorted(config): + if i: + if '::' in i and 'vrf' in i: + ip_str = 'ipv6 route vrf' + elif '::' in i and 'vrf' not in i: + ip_str = 'ipv6 route' + elif '.' in i and 'vrf' in i: + ip_str = 'ip route vrf' + elif '.' in i and 'vrf' not in i: + ip_str = 'ip route' + + if 'vrf' in i: + filter_vrf = utils.parse_conf_arg(i, ip_str) + if '/' not in filter_vrf and '::' not in filter_vrf: + filter_vrf, dest_vrf = self.update_netmask_to_cidr(filter_vrf, 1, 2) + dest_vrf = dest_vrf + '_vrf' + else: + dest_vrf = filter_vrf.split(' ')[1] + if dest_vrf not in same_dest.keys(): + same_dest[dest_vrf] = [] + same_dest[dest_vrf].append('vrf ' + filter_vrf) + elif 'vrf' not in same_dest[dest_vrf][0]: + same_dest[dest_vrf] = [] + same_dest[dest_vrf].append('vrf ' + filter_vrf) + else: + same_dest[dest_vrf].append(('vrf ' + filter_vrf)) + else: + filter = utils.parse_conf_arg(i, ip_str) + if '/' not in filter and '::' not in filter: + filter, dest = self.update_netmask_to_cidr(filter, 0, 1) + else: + dest = filter.split(' ')[0] + if dest not in same_dest.keys(): + same_dest[dest] = [] + same_dest[dest].append(filter) + elif 'vrf' in same_dest[dest][0]: + same_dest[dest] = [] + same_dest[dest].append(filter) + else: + same_dest[dest].append(filter) + return same_dest + + def render_config(self, spec, conf, conf_val): + """ + 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) + config['address_families'] = [] + route_dict = dict() + final_route = dict() + afi = dict() + final_route['routes'] = [] + next_hops = [] + hops = {} + vrf = '' + address_family = dict() + for each in conf_val: + route = each.split(' ') + if 'vrf' in conf_val[0]: + vrf = route[route.index('vrf') + 1] + route_dict['dest'] = conf.split('_')[0] + else: + route_dict['dest'] = conf + if 'vrf' in conf_val[0]: + hops = {} + if '::' in conf: + hops['forward_router_address'] = route[3] + afi['afi'] = 'ipv6' + elif '.' in conf: + hops['forward_router_address'] = route[3] + afi['afi'] = "ipv4" + else: + hops['interface'] = conf + else: + + if '::' in conf: + hops['forward_router_address'] = route[1] + afi['afi'] = 'ipv6' + elif '.' in conf: + hops['forward_router_address'] = route[1] + afi['afi'] = "ipv4" + else: + hops['interface'] = route[1] + try: + temp_list = each.split(' ') + if 'tag' in temp_list: + del temp_list[temp_list.index('tag') + 1] + if 'track' in temp_list: + del temp_list[temp_list.index('track') + 1] + # find distance metric + dist_metrics = int( + [i for i in temp_list if '.' not in i and ':' not in i and ord(i[0]) > 48 and ord(i[0]) < 57][0] + ) + except IndexError: + dist_metrics = None + if dist_metrics: + hops['distance_metric'] = dist_metrics + if 'name' in route: + hops['name'] = route[route.index('name') + 1] + if 'multicast' in route: + hops['multicast'] = True + if 'dhcp' in route: + hops['dhcp'] = True + if 'global' in route: + hops['global'] = True + if 'permanent' in route: + hops['permanent'] = True + if 'tag' in route: + hops['tag'] = route[route.index('tag') + 1] + if 'track' in route: + hops['track'] = route[route.index('track') + 1] + next_hops.append(hops) + hops = {} + route_dict['next_hops'] = next_hops + if route_dict: + final_route['routes'].append(route_dict) + address_family.update(afi) + address_family.update(final_route) + config['address_families'].append(address_family) + if vrf: + config['vrf'] = vrf + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/ios/utils/utils.py b/lib/ansible/module_utils/network/ios/utils/utils.py index a29d814f9db..258a1161901 100644 --- a/lib/ansible/module_utils/network/ios/utils/utils.py +++ b/lib/ansible/module_utils/network/ios/utils/utils.py @@ -28,6 +28,28 @@ def add_command_to_config_list(interface, cmd, commands): commands.append(cmd) +def new_dict_to_set(input_dict, temp_list, test_set, count): + test_dict = dict() + if isinstance(input_dict, dict): + input_dict_len = len(input_dict) + for k, v in sorted(iteritems(input_dict)): + count += 1 + if isinstance(v, list): + temp_list.append(k) + for each in v: + if isinstance(each, dict): + if [True for i in each.values() if type(i) == list]: + new_dict_to_set(each, temp_list, test_set, count) + else: + new_dict_to_set(each, temp_list, test_set, 0) + else: + if v is not None: + test_dict.update({k: v}) + if tuple(iteritems(test_dict)) not in test_set and count == input_dict_len: + test_set.add(tuple(iteritems(test_dict))) + count = 0 + + def dict_to_set(sample_dict): # Generate a set with passed dictionary for comparison test_dict = dict() @@ -162,6 +184,31 @@ def validate_n_expand_ipv4(module, want): return ip_addr_want +def netmask_to_cidr(netmask): + bit_range = [128, 64, 32, 16, 8, 4, 2, 1] + count = 0 + cidr = 0 + netmask_list = netmask.split('.') + netmask_calc = [i for i in netmask_list if int(i) != 255 and int(i) != 0] + if netmask_calc: + netmask_calc_index = netmask_list.index(netmask_calc[0]) + elif sum(list(map(int, netmask_list))) == 0: + return '32' + else: + return '24' + for each in bit_range: + if cidr == int(netmask.split('.')[2]): + if netmask_calc_index == 1: + return str(8 + count) + elif netmask_calc_index == 2: + return str(8 * 2 + count) + elif netmask_calc_index == 3: + return str(8 * 3 + count) + break + cidr += each + count += 1 + + def normalize_interface(name): """Return the normalized interface name """ diff --git a/lib/ansible/modules/network/ios/ios_facts.py b/lib/ansible/modules/network/ios/ios_facts.py index 6a93dc7971e..5c3ff8dae31 100644 --- a/lib/ansible/modules/network/ios/ios_facts.py +++ b/lib/ansible/modules/network/ios/ios_facts.py @@ -58,7 +58,7 @@ options: a specific subset should not be collected. Valid subsets are 'all', 'interfaces', 'l2_interfaces', 'vlans', 'lag_interfaces', 'lacp', 'lacp_interfaces', 'lldp_global', - 'lldp_interfaces', 'l3_interfaces', 'acl_interfaces'. + 'lldp_interfaces', 'l3_interfaces', 'acl_interfaces', 'static_routes'. version_added: "2.9" """ diff --git a/lib/ansible/modules/network/ios/ios_static_routes.py b/lib/ansible/modules/network/ios/ios_static_routes.py new file mode 100644 index 00000000000..dc76d04ffe3 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_static_routes.py @@ -0,0 +1,710 @@ +#!/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 ios_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: ios_static_routes +version_added: "2.10" +short_description: Configure and manage static routes on IOS devices. +description: This module configures and manages the static routes on IOS platforms. +author: Sumit Jaiswal (@justjais) +notes: +- Tested against Cisco IOSv Version 15.2 on VIRL +- This module works with connection C(network_cli). + See L(IOS Platform Options,../network/user_guide/platform_ios.html). +options: + config: + description: A dictionary of static route options + type: list + elements: dict + suboptions: + vrf: + description: + - IP VPN Routing/Forwarding instance name. + - NOTE, In case of IPV4/IPV6 VRF routing table should pre-exist before + configuring. + - NOTE, if the vrf information is not provided then the routes shall be + configured under global vrf. + type: str + address_families: + elements: dict + description: + - Address family to use for the static routes + type: list + suboptions: + afi: + description: + - Top level address family indicator. + required: true + type: str + choices: + - ipv4 + - ipv6 + routes: + description: Configuring static route + type: list + elements: dict + suboptions: + dest: + description: Destination prefix with its subnet mask + type: str + required: true + topology: + description: + - Configure static route for a Topology Routing/Forwarding instance + - NOTE, VRF and Topology can be used together only with Multicast and + Topology should pre-exist before it can be used + type: str + next_hops: + description: + - next hop address or interface + type: list + elements: dict + suboptions: + forward_router_address: + description: Forwarding router's address + type: str + interface: + description: Interface for directly connected static routes + type: str + dhcp: + description: Default gateway obtained from DHCP + type: bool + distance_metric: + description: Distance metric for this route + type: int + global: + description: Next hop address is global + type: bool + name: + description: Specify name of the next hop + type: str + multicast: + description: multicast route + type: bool + permanent: + description: permanent route + type: bool + tag: + description: + - Set tag for this route + - Refer to vendor documentation for valid values. + type: int + track: + description: + - Install route depending on tracked item with tracked object number. + - Tracking does not support multicast + - Refer to vendor documentation for valid values. + type: int + 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 + C(show configuration commands | grep 'static route') + type: str + state: + description: + - The state the configuration should be left in + - The states I(rendered), I(gathered) and I(parsed) does not perform any change on the + device. + - The state I(rendered) will transform the configuration in C(config) option to platform + specific CLI commands which will be returned in the I(rendered) key within the result. + For state I(rendered) active connection to remote host is not required. + - The state I(gathered) will fetch the running configuration from device and transform + it into structured data in the format as per the resource module argspec and the + value is returned in the I(gathered) key within the result. + - The state I(parsed) reads the configuration from C(running_config) option and transforms + it into JSON format as per the resource module parameters and the value is returned in + the I(parsed) key within the result. The value of C(running_config) option should be the + same format as the output of command I(show running-config | include ip route|ipv6 route) + executed on device. For state I(parsed) active connection to remote host is not required. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged +""" + +EXAMPLES = """ +--- + +# Using merged + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route + +- name: Merge provided configuration with device configuration + ios_static_routes: + config: + - vrf: blue + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: merged_blue + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: merged_route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: merged_route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: merged_route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: merged_v6 + tag: 105 + state: merged + +# Commands fired: +# --------------- +# ip route vrf blue 192.0.2.0 255.255.255.0 10.0.0.8 name merged_blue track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name merged_route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name merged_route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name merged_route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name merged_v6 tag 105 + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf blue 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name merged_blue track 150 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name merged_route_3 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name merged_route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name merged_route_1 multicast +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name merged_v6 + +# Using replaced + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Replace provided configuration with device configuration + ios_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: replaced_route + distance_metric: 175 + tag: 70 + multicast: True + state: replaced + +# Commands fired: +# --------------- +# no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 175 name replaced_route track 150 tag 70 + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 175 name replaced_route track 150 tag 70 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 + +# Using overridden + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Override provided configuration with device configuration + ios_static_routes: + config: + - vrf: blue + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: override_vrf + tag: 50 + track: 150 + state: overridden + +# Commands fired: +# --------------- +# no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 198.51.101.8 name test_vrf track 150 tag 50 +# no ipv6 route FD5D:12C9:2201:1::/64 FD5D:12C9:2202::2 name test_v6 tag 105 +# ip route vrf blue 192.0.2.0 255.255.255.0 198.51.101.4 name override_vrf track 150 tag 50 + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf blue 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name override_vrf track 150 + +# Using Deleted + +# Example 1: +# ---------- +# To delete the exact static routes, with all the static routes explicitly mentioned in want + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Delete provided configuration from the device configuration + ios_static_routes: + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: test_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: test_v6 + tag: 105 + state: deleted + +# Commands fired: +# --------------- +# no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 198.51.101.8 name test_vrf track 150 tag 50 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# no ipv6 route FD5D:12C9:2201:1::/64 FD5D:12C9:2202::2 name test_v6 tag 105 + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route + +# Example 2: +# ---------- +# To delete the destination specific static routes + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Delete provided configuration from the device configuration + ios_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + state: deleted + +# Commands fired: +# --------------- +# no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name test_vrf track 150 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 + + +# Example 3: +# ---------- +# To delete the vrf specific static routes + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Delete provided configuration from the device configuration + ios_static_routes: + config: + - vrf: ansible_temp_vrf + state: deleted + +# Commands fired: +# --------------- +# no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 + +# Using Deleted without any config passed +#"(NOTE: This will delete all of configured resource module attributes from each configured interface)" + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Delete ALL configured IOS static routes + ios_static_routes: + state: deleted + +# Commands fired: +# --------------- +# no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name test_vrf track 150 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast +# no ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 + +# After state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# + +# Using gathered + +# Before state: +# ------------- +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +- name: Gather listed static routes with provided configurations + ios_static_routes: + config: + state: gathered + +# Module Execution Result: +# ------------------------ +# +# "gathered": [ +# { +# "address_families": [ +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "192.0.2.0/24", +# "next_hops": [ +# { +# "forward_router_address": "192.0.2.1", +# "name": "test_vrf", +# "tag": 50, +# "track": 150 +# } +# ] +# } +# ] +# } +# ], +# "vrf": "ansible_temp_vrf" +# }, +# { +# "address_families": [ +# { +# "afi": "ipv6", +# "routes": [ +# { +# "dest": "2001:DB8:0:3::/64", +# "next_hops": [ +# { +# "forward_router_address": "2001:DB8:0:3::2", +# "name": "test_v6", +# "tag": 105 +# } +# ] +# } +# ] +# }, +# { +# "afi": "ipv4", +# "routes": [ +# { +# "dest": "198.51.100.0/24", +# "next_hops": [ +# { +# "distance_metric": 110, +# "forward_router_address": "198.51.101.1", +# "multicast": true, +# "name": "route_1", +# "tag": 40 +# }, +# { +# "distance_metric": 30, +# "forward_router_address": "198.51.101.2", +# "name": "route_2" +# }, +# { +# "forward_router_address": "198.51.101.3", +# "name": "route_3" +# } +# ] +# } +# ] +# } +# ] +# } +# ] + +# After state: +# ------------ +# +# vios#show running-config | include ip route|ipv6 route +# ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50 +# ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40 +# ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 +# ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 +# ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105 + +# Using rendered + +- name: Render the commands for provided configuration + ios_static_routes: + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: test_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: test_v6 + tag: 105 + state: rendered + +# Module Execution Result: +# ------------------------ +# +# "rendered": [ +# "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50", +# "ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40", +# "ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2", +# "ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3", +# "ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105" +# ] + +""" + +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: The configuration returned will always be in the same format of the parameters above. +after: + description: The configuration as structured data after module completion. + 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 test 172.31.10.0 255.255.255.0 10.10.10.2 name new_test multicast'] +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: ['interface Ethernet1/1', 'mtu 1800'] +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.ios.argspec.static_routes.static_routes import Static_RoutesArgs +from ansible.module_utils.network.ios.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', 'rendered', ('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/ios_static_routes/defaults/main.yaml b/test/integration/targets/ios_static_routes/defaults/main.yaml new file mode 100644 index 00000000000..164afead284 --- /dev/null +++ b/test/integration/targets/ios_static_routes/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ios_static_routes/meta/main.yaml b/test/integration/targets/ios_static_routes/meta/main.yaml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/test/integration/targets/ios_static_routes/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/test/integration/targets/ios_static_routes/tasks/cli.yaml b/test/integration/targets/ios_static_routes/tasks/cli.yaml new file mode 100644 index 00000000000..337e34133b0 --- /dev/null +++ b/test/integration/targets/ios_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/ios_static_routes/tasks/main.yaml b/test/integration/targets/ios_static_routes/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_static_routes/tests/cli/_intial_setup_config.yaml b/test/integration/targets/ios_static_routes/tests/cli/_intial_setup_config.yaml new file mode 100644 index 00000000000..a6b390ad043 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/_intial_setup_config.yaml @@ -0,0 +1,13 @@ +--- +- name: Intitial Setup Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + vrf definition ansible_temp_vrf + address-family ipv4 + ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name test_vrf track 150 + ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 + ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 + ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast + ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 diff --git a/test/integration/targets/ios_static_routes/tests/cli/_populate_config.yaml b/test/integration/targets/ios_static_routes/tests/cli/_populate_config.yaml new file mode 100644 index 00000000000..e6275cc7e27 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/_populate_config.yaml @@ -0,0 +1,11 @@ +--- +- name: Populate Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name test_vrf track 150 + ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 + ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 + ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast + ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 diff --git a/test/integration/targets/ios_static_routes/tests/cli/_remove_config.yaml b/test/integration/targets/ios_static_routes/tests/cli/_remove_config.yaml new file mode 100644 index 00000000000..acfd6cc8e5d --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/_remove_config.yaml @@ -0,0 +1,12 @@ +--- +- name: Remove Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 tag 50 name test_vrf track 150 + no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3 + no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2 + no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 tag 40 name route_1 multicast + no ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 tag 105 name test_v6 + no vrf definition ansible_temp_vrf diff --git a/test/integration/targets/ios_static_routes/tests/cli/deleted.yaml b/test/integration/targets/ios_static_routes/tests/cli/deleted.yaml new file mode 100644 index 00000000000..2d3967b129d --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/deleted.yaml @@ -0,0 +1,66 @@ +--- +- debug: + msg: "Start Deleted integration state for ios_static_routes ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Delete attributes of provided configured interfaces + ios_static_routes: &deleted + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: test_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: test_v6 + tag: 105 + state: deleted + register: result + + - assert: + that: + - "result.commands|length == 5" + - "result.changed == true" + - "result.commands|symmetric_difference(deleted.commands) == []" + + - name: Delete attributes of all configured interfaces (IDEMPOTENT) + ios_static_routes: *deleted + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result.commands|length == 0" + - "result.changed == false" + + always: + - include_tasks: _populate_config.yaml + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_static_routes/tests/cli/empty_config.yaml b/test/integration/targets/ios_static_routes/tests/cli/empty_config.yaml new file mode 100644 index 00000000000..0cf39e734ae --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/empty_config.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: "START ios_static_routes empty_config.yaml integration tests on connection={{ ansible_connection }}" + +- name: Merged with empty config should give appropriate error message + ios_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 + ios_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 + ios_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: Rendered with empty config should give appropriate error message + ios_static_routes: + config: + state: rendered + register: result + ignore_errors: True + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state rendered' + +- name: Parsed with empty config should give appropriate error message + ios_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' diff --git a/test/integration/targets/ios_static_routes/tests/cli/gathered.yaml b/test/integration/targets/ios_static_routes/tests/cli/gathered.yaml new file mode 100644 index 00000000000..4377f544d00 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/gathered.yaml @@ -0,0 +1,22 @@ +--- +- debug: + msg: "START ios_static_routes gathered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Gather the provided configuration with the exisiting running configuration + ios_static_routes: &gathered + config: + state: gathered + register: result + + - name: Assert that gathered dicts was correctly generated + assert: + that: + - "result['changed'] == false" + + always: + - include_tasks: _remove_config.yaml \ No newline at end of file diff --git a/test/integration/targets/ios_static_routes/tests/cli/merged.yaml b/test/integration/targets/ios_static_routes/tests/cli/merged.yaml new file mode 100644 index 00000000000..26825412ca4 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/merged.yaml @@ -0,0 +1,66 @@ +--- +- debug: + msg: "START Merged ios_static_routes state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Merge provided configuration with device configuration + ios_static_routes: &merged + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: merged_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: merged_route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: merged_route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: merged_route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: merged_v6 + tag: 105 + state: merged + register: result + + - assert: + that: + - "result.commands|length == 5" + - "result.changed == true" + - "result.commands|symmetric_difference(merged.commands) == []" + + - name: Merge provided configuration with device configuration (IDEMPOTENT) + ios_static_routes: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result.commands|length == 0" + - "result['changed'] == false" + + always: + - include_tasks: _populate_config.yaml + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_static_routes/tests/cli/overridden.yaml b/test/integration/targets/ios_static_routes/tests/cli/overridden.yaml new file mode 100644 index 00000000000..02b6acb6a87 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/overridden.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: "START Overridden ios_static_routes state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Override device configuration of all interfaces with provided configuration + ios_static_routes: &overridden + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: override_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.3 + name: override_route + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: override_v6 + tag: 175 + state: overridden + register: result + + - assert: + that: + - "result.commands|length == 8" + - "result.changed == true" + - "result.commands|symmetric_difference(overridden.commands) == []" + + - name: Override device configuration of all interfaces with provided configuration (IDEMPOTENT) + ios_static_routes: *overridden + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result.commands|length == 0" + - "result['changed'] == false" + + always: + - include_tasks: _populate_config.yaml + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_static_routes/tests/cli/rendered.yaml b/test/integration/targets/ios_static_routes/tests/cli/rendered.yaml new file mode 100644 index 00000000000..9805efebaf9 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/rendered.yaml @@ -0,0 +1,54 @@ +--- +- debug: + msg: "START ios_static_routes rendered integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Rendered the provided configuration with the exisiting running configuration + ios_static_routes: &rendered + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: test_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: route_1 + distance_metric: 110 + tag: 40 + multicast: True + - forward_router_address: 198.51.101.2 + name: route_2 + distance_metric: 30 + - forward_router_address: 198.51.101.3 + name: route_3 + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: test_v6 + tag: 105 + state: rendered + register: result + + - assert: + that: + - "result.changed == false" + - "result.rendered|symmetric_difference(rendered.commands) == []" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_static_routes/tests/cli/replaced.yaml b/test/integration/targets/ios_static_routes/tests/cli/replaced.yaml new file mode 100644 index 00000000000..b7e7badfed2 --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/replaced.yaml @@ -0,0 +1,61 @@ +--- +- debug: + msg: "START Replaced ios_static_routes state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _intial_setup_config.yaml + +- block: + - name: Replaces device configuration of listed interfaces with provided configuration + ios_static_routes: &replaced + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: replaced_vrf + tag: 75 + track: 155 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: replaced_route + distance_metric: 175 + tag: 70 + multicast: True + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: replaced_v6 + tag: 110 + state: replaced + register: result + + - assert: + that: + - "result.commands|length == 7" + - "result.changed == true" + - "result.commands|symmetric_difference(replaced.commands) == []" + + - name: Replaces device configuration of listed interfaces with provided configuration (IDEMPOTENT) + ios_static_routes: *replaced + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result.commands|length == 0" + - "result['changed'] == false" + + always: + - include_tasks: _populate_config.yaml + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_static_routes/tests/cli/rtt.yaml b/test/integration/targets/ios_static_routes/tests/cli/rtt.yaml new file mode 100644 index 00000000000..e8a5c6b151c --- /dev/null +++ b/test/integration/targets/ios_static_routes/tests/cli/rtt.yaml @@ -0,0 +1,78 @@ +--- +- debug: + msg: "START ios_static_routes round trip integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + - name: Apply the provided configuration (base config) + ios_static_routes: + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: rtt_vrf + tag: 50 + track: 150 + - address_families: + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - forward_router_address: 198.51.101.1 + name: rtt_route_1 + distance_metric: 110 + tag: 40 + multicast: True + state: merged + register: base_config + + - name: Gather static routes facts + ios_facts: + gather_subset: + - "!all" + - "!min" + gather_network_resources: + - static_routes + + - name: Apply the configuration which need to be reverted + ios_static_routes: + config: + - vrf: ansible_temp_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.12 + name: new_rtt_vrf + tag: 10 + track: 150 + state: overridden + register: result + + - assert: + that: + - "result.commands|length == 2" + - "result.changed == true" + - "result.commands|symmetric_difference(rtt.override_commands) == []" + + - name: Revert back to base config using facts round trip + ios_static_routes: + config: "{{ ansible_facts['network_resources']['static_routes'] }}" + state: overridden + register: revert + + - assert: + that: + - "revert.commands|length == 1" + - "revert.changed == true" + - "revert.commands|symmetric_difference(rtt.override_revert_commands) == []" + + always: + - include_tasks: _populate_config.yaml + - include_tasks: _remove_config.yaml \ No newline at end of file diff --git a/test/integration/targets/ios_static_routes/vars/main.yaml b/test/integration/targets/ios_static_routes/vars/main.yaml new file mode 100644 index 00000000000..30c67c56e95 --- /dev/null +++ b/test/integration/targets/ios_static_routes/vars/main.yaml @@ -0,0 +1,88 @@ +--- +deleted: + commands: + - "no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3" + - "no ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105" + +merged: + commands: + - "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name merged_vrf track 150 tag 50" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name merged_route_1 tag 40" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name merged_route_2" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.3 name merged_route_3" + - "ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name merged_v6 tag 105" + +replaced: + commands: + - "no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3" + - "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name replaced_vrf track 155 tag 75" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.1 175 multicast name replaced_route tag 70" + - "ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name replaced_v6 tag 110" + +overridden: + commands: + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2" + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3" + - "no ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50" + - "no ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.3 name override_route" + - "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name override_vrf track 150 tag 50" + - "ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name override_v6 tag 175" + +rendered: + commands: + - "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.2 30 name route_2" + - "ip route 198.51.100.0 255.255.255.0 198.51.101.3 name route_3" + - "ipv6 route 2001:DB8:0:3::/64 2001:DB8:0:3::2 name test_v6 tag 105" + +gathered: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.0/24 + next_hops: + - forward_router_address: 192.0.2.1 + name: test_vrf + tag: 50 + track: 150 + vrf: ansible_temp_vrf + - address_families: + - afi: ipv6 + routes: + - dest: 2001:DB8:0:3::/64 + next_hops: + - forward_router_address: 2001:DB8:0:3::2 + name: test_v6 + tag: 105 + - afi: ipv4 + routes: + - dest: 198.51.100.0/24 + next_hops: + - distance_metric: 110 + forward_router_address: 198.51.101.1 + multicast: true + name: route_1 + tag: 40 + - distance_metric: 30 + forward_router_address: 198.51.101.2 + name: route_2 + - forward_router_address: 198.51.101.3 + name: route_3 + +rtt: + override_commands: + - "no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name rtt_route_1 tag 40" + - "ip route vrf ansible_temp_vrf 192.0.2.0 255.255.255.0 192.0.2.12 name new_rtt_vrf track 150 tag 10" + + override_revert_commands: + - "ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name rtt_route_1 tag 40" diff --git a/test/units/modules/network/ios/fixtures/ios_static_routes_config.cfg b/test/units/modules/network/ios/fixtures/ios_static_routes_config.cfg new file mode 100644 index 00000000000..b947d5dcfef --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_static_routes_config.cfg @@ -0,0 +1,2 @@ +ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 175 tag 50 +ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 60 diff --git a/test/units/modules/network/ios/test_ios_static_routes.py b/test/units/modules/network/ios/test_ios_static_routes.py new file mode 100644 index 00000000000..50b0ffa7ecd --- /dev/null +++ b/test/units/modules/network/ios/test_ios_static_routes.py @@ -0,0 +1,357 @@ +# +# (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.ios import ios_static_routes +from units.modules.utils import set_module_args +from .ios_module import TestIosModule, load_fixture + + +class TestIosStaticRoutesModule(TestIosModule): + module = ios_static_routes + + def setUp(self): + super(TestIosStaticRoutesModule, 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.ios.providers.providers.CliProvider.edit_config') + self.edit_config = self.mock_edit_config.start() + + self.mock_execute_show_command = patch('ansible.module_utils.network.ios.facts.static_routes.static_routes.' + 'Static_RoutesFacts.get_static_routes_data') + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosStaticRoutesModule, 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'): + def load_from_file(*args, **kwargs): + return load_fixture('ios_static_routes_config.cfg') + self.execute_show_command.side_effect = load_from_file + + def test_ios_static_routes_merged(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0 255.255.255.0", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=150 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0 255.255.255.0", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="route_1", + distance_metric=110, + tag=40, + multicast=True + )], + )], + )], + )], state="merged" + )) + result = self.execute_module(changed=True) + commands = ['ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 150 tag 50', + 'ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 40' + ] + self.assertEqual(result['commands'], commands) + + def test_ios_static_routes_merged_idempotent(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=175 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0/24", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="route_1", + distance_metric=110, + tag=60, + multicast=True + )], + )], + )], + )], state="merged" + )) + self.execute_module(changed=False, commands=[], sort=True) + + def test_ios_static_routes_replaced(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0 255.255.255.0", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="replaced_vrf", + tag=10, + track=170 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0 255.255.255.0", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="replaced_route_1", + distance_metric=110, + tag=60, + multicast=True + )], + )], + )], + )], state="replaced" + )) + result = self.execute_module(changed=True) + commands = ['ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name replaced_vrf track 170 tag 10', + 'ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name replaced_route_1 tag 60' + ] + self.assertEqual(result['commands'], commands) + + def test_ios_static_routes_replaced_idempotent(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=175 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0/24", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="route_1", + distance_metric=110, + tag=60, + multicast=True + )], + )], + )], + )], state="replaced" + )) + self.execute_module(changed=False, commands=[], sort=True) + + def test_ios_static_routes_overridden(self): + set_module_args(dict( + config=[dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0 255.255.255.0", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="override_route_1", + distance_metric=150, + tag=50, + multicast=True + )], + )], + )], + )], state="overridden" + )) + result = self.execute_module(changed=True) + commands = [ + 'no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 60', + 'no ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 175 tag 50', + 'ip route 198.51.100.0 255.255.255.0 198.51.101.1 150 multicast name override_route_1 tag 50' + ] + + self.assertEqual(result['commands'], commands) + + def test_ios_static_routes_overridden_idempotent(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=175 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0/24", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="route_1", + distance_metric=110, + tag=60, + multicast=True + )], + )], + )], + )], state="overridden" + )) + self.execute_module(changed=False, commands=[], sort=True) + + def test_ios_delete_static_route_config(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=175 + )], + )], + )], + ), dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0/24", + next_hops=[dict( + forward_router_address="198.51.101.1", + name="route_1", + distance_metric=110, + tag=60, + multicast=True + )], + )], + )], + )], state="deleted" + )) + result = self.execute_module(changed=True) + commands = [ + 'no ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 175 tag 50', + 'no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 60' + ] + self.assertEqual(result['commands'], commands) + + def test_ios_delete_static_route_dest_based(self): + set_module_args(dict( + config=[dict( + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="198.51.100.0/24" + )], + )], + )], state="deleted" + )) + result = self.execute_module(changed=True) + commands = [ + 'no ip route 198.51.100.0 255.255.255.0 198.51.101.1 110 multicast name route_1 tag 60' + ] + self.assertEqual(result['commands'], commands) + + def test_ios_delete_static_route_vrf_based(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24" + )], + )], + )], state="deleted" + )) + result = self.execute_module(changed=True) + commands = [ + 'no ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 175 tag 50' + ] + self.assertEqual(result['commands'], commands) + + def test_static_route_rendered(self): + set_module_args(dict( + config=[dict( + vrf="ansible_vrf", + address_families=[dict( + afi="ipv4", + routes=[dict( + dest="192.0.2.0/24", + next_hops=[dict( + forward_router_address="192.0.2.1", + name="test_vrf", + tag=50, + track=175 + )], + )], + )], + )], state="rendered" + )) + commands = [ + 'ip route vrf ansible_vrf 192.0.2.0 255.255.255.0 192.0.2.1 name test_vrf track 175 tag 50' + ] + result = self.execute_module(changed=False) + self.assertEqual(sorted(result['rendered']), commands)