From f615011de3293fbca000b7ae47992e42cf6d874d Mon Sep 17 00:00:00 2001 From: Nilashish Chakraborty Date: Tue, 12 Mar 2019 17:47:40 +0530 Subject: [PATCH] New module for BGP configuration management in iosxr (#53121) * Add iosxr_bgp module Signed-off-by: NilashishC * Remove unused code Signed-off-by: NilashishC * Remove netconf code Signed-off-by: NilashishC --- .../network/iosxr/providers/__init__.py | 0 .../network/iosxr/providers/cli/__init__.py | 0 .../iosxr/providers/cli/config/__init__.py | 0 .../providers/cli/config/bgp/__init__.py | 0 .../cli/config/bgp/address_family.py | 114 +++++++ .../providers/cli/config/bgp/neighbors.py | 125 ++++++++ .../iosxr/providers/cli/config/bgp/process.py | 97 ++++++ .../network/iosxr/providers/module.py | 62 ++++ .../network/iosxr/providers/providers.py | 120 +++++++ .../modules/network/iosxr/iosxr_bgp.py | 295 ++++++++++++++++++ .../targets/iosxr_bgp/defaults/main.yaml | 3 + .../targets/iosxr_bgp/meta/main.yaml | 2 + .../targets/iosxr_bgp/tasks/cli.yaml | 16 + .../targets/iosxr_bgp/tasks/main.yaml | 2 + .../targets/iosxr_bgp/tests/cli/basic.yaml | 226 ++++++++++++++ 15 files changed, 1062 insertions(+) create mode 100644 lib/ansible/module_utils/network/iosxr/providers/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/config/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/__init__.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/address_family.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/neighbors.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/process.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/module.py create mode 100644 lib/ansible/module_utils/network/iosxr/providers/providers.py create mode 100644 lib/ansible/modules/network/iosxr/iosxr_bgp.py create mode 100644 test/integration/targets/iosxr_bgp/defaults/main.yaml create mode 100644 test/integration/targets/iosxr_bgp/meta/main.yaml create mode 100644 test/integration/targets/iosxr_bgp/tasks/cli.yaml create mode 100644 test/integration/targets/iosxr_bgp/tasks/main.yaml create mode 100644 test/integration/targets/iosxr_bgp/tests/cli/basic.yaml diff --git a/lib/ansible/module_utils/network/iosxr/providers/__init__.py b/lib/ansible/module_utils/network/iosxr/providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/__init__.py b/lib/ansible/module_utils/network/iosxr/providers/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/config/__init__.py b/lib/ansible/module_utils/network/iosxr/providers/cli/config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/__init__.py b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/address_family.py b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/address_family.py new file mode 100644 index 00000000000..2005615d97d --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/address_family.py @@ -0,0 +1,114 @@ +# +# (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) +# +import re + +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.iosxr.providers.providers import CliProvider + + +class AddressFamily(CliProvider): + + def render(self, config=None): + commands = list() + safe_list = list() + + router_context = 'router bgp %s' % self.get_value('config.bgp_as') + context_config = None + + for item in self.get_value('config.address_family'): + context = 'address-family %s %s' % (item['afi'], item['safi']) + context_commands = list() + + if config: + context_path = [router_context, context] + context_config = self.get_config_context(config, context_path, indent=1) + + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, '_render_%s' % key, None) + if meth: + resp = meth(item, context_config) + if resp: + context_commands.extend(to_list(resp)) + + if context_commands: + commands.append(context) + commands.extend(context_commands) + commands.append('exit') + + safe_list.append(context) + + if config: + resp = self._negate_config(config, safe_list) + commands.extend(resp) + + return commands + + def _negate_config(self, config, safe_list=None): + commands = list() + matches = re.findall(r'(address-family .+)$', config, re.M) + for item in set(matches).difference(safe_list): + commands.append('no %s' % item) + return commands + + def _render_networks(self, item, config=None): + commands = list() + safe_list = list() + + for entry in item['networks']: + network = entry['prefix'] + if entry['masklen']: + network = '%s/%s' % (entry['prefix'], entry['masklen']) + safe_list.append(network) + + cmd = 'network %s' % network + + if entry['route_map']: + cmd += ' route-policy %s' % entry['route_map'] + + if not config or cmd not in config: + commands.append(cmd) + + if config and self.params['operation'] == 'replace': + matches = re.findall(r'network (\S+)', config, re.M) + for entry in set(matches).difference(safe_list): + commands.append('no network %s' % entry) + + return commands + + def _render_redistribute(self, item, config=None): + commands = list() + safe_list = list() + + for entry in item['redistribute']: + option = entry['protocol'] + + cmd = 'redistribute %s' % entry['protocol'] + + if entry['id'] and entry['protocol'] in ('ospf', 'eigrp', 'isis', 'ospfv3'): + cmd += ' %s' % entry['id'] + option += ' %s' % entry['id'] + + if entry['metric']: + cmd += ' metric %s' % entry['metric'] + + if entry['route_map']: + cmd += ' route-policy %s' % entry['route_map'] + + if not config or cmd not in config: + commands.append(cmd) + + safe_list.append(option) + + if self.params['operation'] == 'replace': + if config: + matches = re.findall(r'redistribute (\S+)(?:\s*)(\d*)', config, re.M) + for i in range(0, len(matches)): + matches[i] = ' '.join(matches[i]).strip() + for entry in set(matches).difference(safe_list): + commands.append('no redistribute %s' % entry) + + return commands diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/neighbors.py b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/neighbors.py new file mode 100644 index 00000000000..137691e3e1d --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/neighbors.py @@ -0,0 +1,125 @@ +# +# (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) +# +import re +import socket + +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.iosxr.providers.providers import CliProvider + + +class Neighbors(CliProvider): + + def render(self, config=None): + commands = list() + safe_list = list() + + router_context = 'router bgp %s' % self.get_value('config.bgp_as') + context_config = None + + for item in self.get_value('config.neighbors'): + context_commands = list() + + neighbor = item['neighbor'] + + try: + socket.inet_aton(neighbor) + context = 'neighbor %s' % neighbor + except socket.error: + context = 'neighbor-group %s' % neighbor + + if config: + context_path = [router_context, context] + context_config = self.get_config_context(config, context_path, indent=1) + + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, '_render_%s' % key, None) + if meth: + resp = meth(item, context_config) + if resp: + context_commands.extend(to_list(resp)) + + if context_commands: + commands.append(context) + commands.extend(context_commands) + commands.append('exit') + + safe_list.append(context) + + if config and safe_list: + commands.extend(self._negate_config(config, safe_list)) + + return commands + + def _negate_config(self, config, safe_list=None): + commands = list() + matches = re.findall(r'(neighbor \S+)', config, re.M) + for item in set(matches).difference(safe_list): + commands.append('no %s' % item) + return commands + + def _render_remote_as(self, item, config=None): + cmd = 'remote-as %s' % item['remote_as'] + if not config or cmd not in config: + return cmd + + def _render_description(self, item, config=None): + cmd = 'description %s' % item['description'] + if not config or cmd not in config: + return cmd + + def _render_enabled(self, item, config=None): + cmd = 'shutdown' + if item['enabled'] is True: + cmd = 'no %s' % cmd + if not config or cmd not in config: + return cmd + + def _render_update_source(self, item, config=None): + cmd = 'update-source %s' % item['update_source'].replace(' ', '') + if not config or cmd not in config: + return cmd + + def _render_password(self, item, config=None): + cmd = 'password %s' % item['password'] + if not config or cmd not in config: + return cmd + + def _render_ebgp_multihop(self, item, config=None): + cmd = 'ebgp-multihop %s' % item['ebgp_multihop'] + if not config or cmd not in config: + return cmd + + def _render_tcp_mss(self, item, config=None): + cmd = 'tcp mss %s' % item['tcp_mss'] + if not config or cmd not in config: + return cmd + + def _render_advertisement_interval(self, item, config=None): + cmd = 'advertisement-interval %s' % item['advertisement_interval'] + if not config or cmd not in config: + return cmd + + def _render_neighbor_group(self, item, config=None): + cmd = 'use neighbor-group %s' % item['neighbor_group'] + if not config or cmd not in config: + return cmd + + def _render_timers(self, item, config): + """generate bgp timer related configuration + """ + keepalive = item['timers']['keepalive'] + holdtime = item['timers']['holdtime'] + min_neighbor_holdtime = item['timers']['min_neighbor_holdtime'] + + if keepalive and holdtime: + cmd = 'timers %s %s' % (keepalive, holdtime) + if min_neighbor_holdtime: + cmd += ' %s' % min_neighbor_holdtime + if not config or cmd not in config: + return cmd + else: + raise ValueError("required both options for timers: keepalive and holdtime") diff --git a/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/process.py b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/process.py new file mode 100644 index 00000000000..470d586a641 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/providers/cli/config/bgp/process.py @@ -0,0 +1,97 @@ +# +# (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) +# +import re + +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.iosxr.providers.providers import register_provider +from ansible.module_utils.network.iosxr.providers.providers import CliProvider +from ansible.module_utils.network.iosxr.providers.cli.config.bgp.neighbors import Neighbors +from ansible.module_utils.network.iosxr.providers.cli.config.bgp.address_family import AddressFamily + +REDISTRIBUTE_PROTOCOLS = frozenset(['ospf', 'ospfv3', 'eigrp', 'isis', 'static', + 'connected', 'lisp', 'mobile', 'rip', + 'subscriber']) + + +@register_provider('iosxr', 'iosxr_bgp') +class Provider(CliProvider): + + def render(self, config=None): + commands = list() + + existing_as = None + if config: + match = re.search(r'router bgp (\d+)', config, re.M) + if match: + existing_as = match.group(1) + + operation = self.params['operation'] + + context = None + + if self.params['config']: + context = 'router bgp %s' % self.get_value('config.bgp_as') + + if operation == 'delete': + if existing_as: + commands.append('no router bgp %s' % existing_as) + elif context: + commands.append('no %s' % context) + + else: + if operation == 'replace': + if existing_as and int(existing_as) != self.get_value('config.bgp_as'): + # The negate command has to be committed before new BGP AS is used. + self.connection.edit_config('no router bgp %s' % existing_as) + config = None + + elif operation == 'override': + if existing_as: + # The negate command has to be committed before new BGP AS is used. + self.connection.edit_config('no router bgp %s' % existing_as) + config = None + + context_commands = list() + + for key, value in iteritems(self.get_value('config')): + if value is not None: + meth = getattr(self, '_render_%s' % key, None) + if meth: + resp = meth(config) + if resp: + context_commands.extend(to_list(resp)) + + if context and context_commands: + commands.append(context) + commands.extend(context_commands) + commands.append('exit') + + return commands + + def _render_router_id(self, config=None): + cmd = 'bgp router-id %s' % self.get_value('config.router_id') + if not config or cmd not in config: + return cmd + + def _render_log_neighbor_changes(self, config=None): + cmd = 'bgp log neighbor changes' + log_neighbor_changes = self.get_value('config.log_neighbor_changes') + if log_neighbor_changes is True: + if not config or cmd not in config: + return '%s detail' % cmd + elif log_neighbor_changes is False: + if config and cmd in config: + return '%s disable' % cmd + + def _render_neighbors(self, config): + """ generate bgp neighbor configuration + """ + return Neighbors(self.params).render(config) + + def _render_address_family(self, config): + """ generate address-family configuration + """ + return AddressFamily(self.params).render(config) diff --git a/lib/ansible/module_utils/network/iosxr/providers/module.py b/lib/ansible/module_utils/network/iosxr/providers/module.py new file mode 100644 index 00000000000..28725e9a8e3 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/providers/module.py @@ -0,0 +1,62 @@ +# +# (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 ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection +from ansible.module_utils.network.iosxr.providers import providers +from ansible.module_utils._text import to_text + + +class NetworkModule(AnsibleModule): + + fail_on_missing_provider = True + + def __init__(self, connection=None, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + + if connection is None: + connection = Connection(self._socket_path) + + self.connection = connection + + @property + def provider(self): + if not hasattr(self, '_provider'): + capabilities = self.from_json(self.connection.get_capabilities()) + + network_os = capabilities['device_info']['network_os'] + network_api = capabilities['network_api'] + + if network_api == 'cliconf': + connection_type = 'network_cli' + + cls = providers.get(network_os, self._name, connection_type) + + if not cls: + msg = 'unable to find suitable provider for network os %s' % network_os + if self.fail_on_missing_provider: + self.fail_json(msg=msg) + else: + self.warn(msg) + + obj = cls(self.params, self.connection, self.check_mode) + + setattr(self, '_provider', obj) + + return getattr(self, '_provider') + + def get_facts(self, subset=None): + try: + self.provider.get_facts(subset) + except Exception as exc: + self.fail_json(msg=to_text(exc)) + + def edit_config(self, config_filter=None): + current_config = self.connection.get_config(flags=config_filter) + try: + commands = self.provider.edit_config(current_config) + changed = bool(commands) + return {'commands': commands, 'changed': changed} + except Exception as exc: + self.fail_json(msg=to_text(exc)) diff --git a/lib/ansible/module_utils/network/iosxr/providers/providers.py b/lib/ansible/module_utils/network/iosxr/providers/providers.py new file mode 100644 index 00000000000..a466b033d94 --- /dev/null +++ b/lib/ansible/module_utils/network/iosxr/providers/providers.py @@ -0,0 +1,120 @@ +# +# (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) +# +import json + +from threading import RLock + +from ansible.module_utils.six import itervalues +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.common.config import NetworkConfig + + +_registered_providers = {} +_provider_lock = RLock() + + +def register_provider(network_os, module_name): + def wrapper(cls): + _provider_lock.acquire() + try: + if network_os not in _registered_providers: + _registered_providers[network_os] = {} + for ct in cls.supported_connections: + if ct not in _registered_providers[network_os]: + _registered_providers[network_os][ct] = {} + for item in to_list(module_name): + for entry in itervalues(_registered_providers[network_os]): + entry[item] = cls + finally: + _provider_lock.release() + return cls + return wrapper + + +def get(network_os, module_name, connection_type): + network_os_providers = _registered_providers.get(network_os) + if network_os_providers is None: + raise ValueError('unable to find a suitable provider for this module') + if connection_type not in network_os_providers: + raise ValueError('provider does not support this connection type') + elif module_name not in network_os_providers[connection_type]: + raise ValueError('could not find a suitable provider for this module') + return network_os_providers[connection_type][module_name] + + +class ProviderBase(object): + + supported_connections = () + + def __init__(self, params, connection=None, check_mode=False): + self.params = params + self.connection = connection + self.check_mode = check_mode + + @property + def capabilities(self): + if not hasattr(self, '_capabilities'): + resp = self.from_json(self.connection.get_capabilities()) + setattr(self, '_capabilities', resp) + return getattr(self, '_capabilities') + + def get_value(self, path): + params = self.params.copy() + for key in path.split('.'): + params = params[key] + return params + + def get_facts(self, subset=None): + raise NotImplementedError(self.__class__.__name__) + + def edit_config(self): + raise NotImplementedError(self.__class__.__name__) + + +class CliProvider(ProviderBase): + + supported_connections = ('network_cli',) + + @property + def capabilities(self): + if not hasattr(self, '_capabilities'): + resp = self.from_json(self.connection.get_capabilities()) + setattr(self, '_capabilities', resp) + return getattr(self, '_capabilities') + + def get_config_context(self, config, path, indent=1): + if config is not None: + netcfg = NetworkConfig(indent=indent, contents=config) + try: + config = netcfg.get_block_config(to_list(path)) + except ValueError: + config = None + return config + + def render(self, config=None): + raise NotImplementedError(self.__class__.__name__) + + def cli(self, command): + try: + if not hasattr(self, '_command_output'): + setattr(self, '_command_output', {}) + return self._command_output[command] + except KeyError: + out = self.connection.get(command) + try: + out = json.loads(out) + except ValueError: + pass + self._command_output[command] = out + return out + + def get_facts(self, subset=None): + return self.populate() + + def edit_config(self, config=None): + commands = self.render(config) + if commands and self.check_mode is False: + self.connection.edit_config(commands) + return commands diff --git a/lib/ansible/modules/network/iosxr/iosxr_bgp.py b/lib/ansible/modules/network/iosxr/iosxr_bgp.py new file mode 100644 index 00000000000..865f9dbf68d --- /dev/null +++ b/lib/ansible/modules/network/iosxr/iosxr_bgp.py @@ -0,0 +1,295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: iosxr_bgp +version_added: "2.8" +author: "Nilashish Chakraborty (@nilashishc)" +short_description: Configure global BGP protocol settings on Cisco IOS-XR +description: + - This module provides configuration management of global BGP parameters + on devices running Cisco IOS-XR +notes: + - Tested against Cisco IOS XR Software Version 6.1.3 +options: + config: + description: + - Specifies the BGP related configuration. + suboptions: + bgp_as: + description: + - Specifies the BGP Autonomous System (AS) number to configure on the device. + type: int + required: true + router_id: + description: + - Configures the BGP routing process router-id value. + default: null + log_neighbor_changes: + description: + - Enable/disable logging neighbor up/down and reset reason. + type: bool + neighbors: + description: + - Specifies BGP neighbor related configurations. + suboptions: + neighbor: + description: + - Neighbor router address. + required: True + remote_as: + description: + - Remote AS of the BGP neighbor to configure. + type: int + required: True + update_source: + description: + - Source of the routing updates. + password: + description: + - Password to authenticate the BGP peer connection. + enabled: + description: + - Administratively shutdown or enable a neighbor. + type: bool + description: + description: + - Neighbor specific description. + advertisement_interval: + description: + - Specifies the minimum interval (in seconds) between sending BGP routing updates. + - The range is from 0 to 600. + type: int + tcp_mss: + description: + - Specifies the TCP initial maximum segment size to use. + - The range is from 68 to 10000. + type: int + ebgp_multihop: + description: + - Specifies the maximum hop count for EBGP neighbors not on directly connected networks. + - The range is from 0 to 255. + type: int + timers: + description: + - Specifies BGP neighbor timer related configurations. + suboptions: + keepalive: + description: + - Frequency with which the Cisco IOS-XR software sends keepalive messages to its peer. + - The range is from 0 to 65535. + type: int + required: True + holdtime: + description: + - Interval after not receiving a keepalive message that the software declares a peer dead. + - The range is from 3 to 65535. + type: int + required: True + min_neighbor_holdtime: + description: + - Interval specifying the minimum acceptable hold-time from a BGP neighbor. + - The minimum acceptable hold-time must be less than, or equal to, the interval specified in the holdtime argument. + - The range is from 3 to 65535. + type: int + address_family: + description: + - Specifies BGP address family related configurations. + suboptions: + afi: + description: + - Type of address family to configure. + choices: + - ipv4 + - ipv6 + required: True + safi: + description: + - Specifies the type of cast for the address family. + choices: + - flowspec + - unicast + - multicast + - labeled-unicast + default: unicast + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + choices: ['ospf', 'ospfv3', 'eigrp', 'isis', 'static', 'connected', 'lisp', 'mobile', 'rip', 'subscriber'] + required: True + id: + description: + - Identifier for the routing protocol for configuring redistribute information. + - Valid for protocols 'ospf', 'eigrp', 'isis' and 'ospfv3'. + metric: + description: + - Specifies the metric for redistributed routes. + route_map: + description: + - Specifies the route map reference. + networks: + description: + - Specify networks to announce via BGP. + - For operation replace, this option is mutually exclusive with root level networks option. + suboptions: + network: + description: + - Network ID to announce via BGP. + required: True + masklen: + description: + - Subnet mask length for the network to announce(e.g, 8, 16, 24, etc.). + route_map: + description: + - Route map to modify the attributes. + operation: + description: + - Specifies the operation to be performed on the BGP process configured on the device. + - In case of merge, the input configuration will be merged with the existing BGP configuration on the device. + - In case of replace, if there is a diff between the existing configuration and the input configuration, the + existing configuration will be replaced by the input configuration for every option that has the diff. + - In case of override, all the existing BGP configuration will be removed from the device and replaced with + the input configuration. + - In case of delete the existing BGP configuration will be removed from the device. + default: merge + choices: ['merge', 'replace', 'override', 'delete'] +""" + +EXAMPLES = """ +- name: configure global bgp as 65000 + iosxr_bgp: + bgp_as: 65000 + router_id: 1.1.1.1 + neighbors: + - neighbor: 182.168.10.1 + remote_as: 500 + description: PEER_1 + - neighbor: 192.168.20.1 + remote_as: 500 + update_source: GigabitEthernet 0/0/0/0 + address_family: + - name: ipv4 + cast: unicast + networks: + - network: 192.168.2.0/23 + - network: 10.0.0.0/8 + redistribute: + - protocol: ospf + id: 400 + metric: 110 + +- name: remove bgp as 65000 from config + ios_bgp: + bgp_as: 65000 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - router bgp 65000 + - bgp router-id 1.1.1.1 + - neighbor 182.168.10.1 remote-as 500 + - neighbor 182.168.10.1 description PEER_1 + - neighbor 192.168.20.1 remote-as 500 + - neighbor 192.168.20.1 update-source GigabitEthernet0/0/0/0 + - address-family ipv4 unicast + - redistribute ospf 400 metric 110 + - network 192.168.2.0/23 + - network 10.0.0.0/8 + - exit +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.network.iosxr.providers.module import NetworkModule +from ansible.module_utils.network.iosxr.providers.cli.config.bgp.process import REDISTRIBUTE_PROTOCOLS + + +def main(): + """ main entry point for module execution + """ + network_spec = { + 'prefix': dict(required=True), + 'masklen': dict(type='int', required=True), + 'route_map': dict(), + } + + redistribute_spec = { + 'protocol': dict(choices=REDISTRIBUTE_PROTOCOLS, required=True), + 'id': dict(), + 'metric': dict(type='int'), + 'route_map': dict(), + } + + timer_spec = { + 'keepalive': dict(type='int'), + 'holdtime': dict(type='int'), + 'min_neighbor_holdtime': dict(type='int'), + } + + neighbor_spec = { + 'neighbor': dict(required=True), + 'remote_as': dict(type='int', required=True), + 'update_source': dict(), + 'password': dict(no_log=True), + 'enabled': dict(type='bool'), + 'description': dict(), + 'advertisement_interval': dict(type='int'), + 'ebgp_multihop': dict(type='int'), + 'tcp_mss': dict(type='int'), + 'timers': dict(type='dict', options=timer_spec), + } + + address_family_spec = { + 'afi': dict(choices=['ipv4', 'ipv6'], required=True), + 'safi': dict(choices=['flowspec', 'labeled-unicast', 'multicast', 'unicast'], default='unicast'), + 'networks': dict(type='list', elements='dict', options=network_spec), + 'redistribute': dict(type='list', elements='dict', options=redistribute_spec), + } + + config_spec = { + 'bgp_as': dict(type='int', required=True), + 'router_id': dict(), + 'log_neighbor_changes': dict(type='bool'), + 'neighbors': dict(type='list', elements='dict', options=neighbor_spec), + 'address_family': dict(type='list', elements='dict', options=address_family_spec), + } + + argument_spec = { + 'config': dict(type='dict', options=config_spec), + 'operation': dict(default='merge', choices=['merge', 'replace', 'override', 'delete']) + } + + module = NetworkModule(argument_spec=argument_spec, + supports_check_mode=True) + + try: + result = module.edit_config(config_filter='router bgp') + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/iosxr_bgp/defaults/main.yaml b/test/integration/targets/iosxr_bgp/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/iosxr_bgp/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/iosxr_bgp/meta/main.yaml b/test/integration/targets/iosxr_bgp/meta/main.yaml new file mode 100644 index 00000000000..d4da833dd50 --- /dev/null +++ b/test/integration/targets/iosxr_bgp/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_iosxr_tests diff --git a/test/integration/targets/iosxr_bgp/tasks/cli.yaml b/test/integration/targets/iosxr_bgp/tasks/cli.yaml new file mode 100644 index 00000000000..3f93a4f3696 --- /dev/null +++ b/test/integration/targets/iosxr_bgp/tasks/cli.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_bgp/tasks/main.yaml b/test/integration/targets/iosxr_bgp/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/iosxr_bgp/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/iosxr_bgp/tests/cli/basic.yaml b/test/integration/targets/iosxr_bgp/tests/cli/basic.yaml new file mode 100644 index 00000000000..a3dca3e00e1 --- /dev/null +++ b/test/integration/targets/iosxr_bgp/tests/cli/basic.yaml @@ -0,0 +1,226 @@ +--- +- debug: msg="START iosxr cli/iosxr_bgp.yaml on connection={{ ansible_connection }}" + +- name: Clear existing BGP config + iosxr_bgp: + operation: delete + ignore_errors: yes + +- name: Configure BGP with AS 64496 and a router-id + iosxr_bgp: &config + operation: merge + config: + bgp_as: 64496 + router_id: 192.0.2.2 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'bgp router-id 192.0.2.2' in result.commands" + +- name: Configure BGP with AS 64496 and a router-id (idempotent) + iosxr_bgp: *config + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors + iosxr_bgp: &nbr + operation: merge + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + description: IBGP_NBR_1 + advertisement_interval: 120 + timers: + keepalive: 300 + holdtime: 360 + + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + tcp_mss: 1500 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'neighbor 192.0.2.10' in result.commands" + - "'remote-as 64496' in result.commands" + - "'description IBGP_NBR_1' in result.commands" + - "'timers 300 360' in result.commands" + - "'advertisement-interval 120' in result.commands" + - "'neighbor 192.0.2.15' in result.commands" + - "'remote-as 64496' in result.commands" + - "'description IBGP_NBR_2' in result.commands" + - "'tcp mss 1500' in result.commands" + +- name: Configure BGP neighbors (idempotent) + iosxr_bgp: *nbr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors with operation replace + iosxr_bgp: &nbr_rplc + operation: replace + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + tcp_mss: 1500 + + - neighbor: 203.0.113.10 + remote_as: 64511 + description: EBGP_NBR_1 + register: result + +- assert: + that: + - 'result.changed == true' + - "'neighbor 203.0.113.10' in result.commands" + - "'remote-as 64511' in result.commands" + - "'description EBGP_NBR_1' in result.commands" + - "'no neighbor 192.0.2.10' in result.commands" + +- name: Configure BGP neighbors with operation replace (idempotent) + iosxr_bgp: *nbr_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family + iosxr_bgp: &af_net + operation: merge + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + networks: + - prefix: 198.51.100.48 + masklen: 28 + route_map: RMAP_1 + + - prefix: 192.0.2.64 + masklen: 27 + + - prefix: 203.0.113.160 + masklen: 27 + + - afi: ipv4 + safi: multicast + networks: + - prefix: 198.51.100.64 + masklen: 28 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4 unicast' in result.commands" + - "'network 198.51.100.48/28 route-policy RMAP_1' in result.commands" + - "'network 192.0.2.64/27' in result.commands" + - "'network 203.0.113.160/27' in result.commands" + - "'address-family ipv4 multicast' in result.commands" + - "'network 198.51.100.64/28' in result.commands" + +- name: Configure networks under address family (idempotent) + iosxr_bgp: *af_net + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family with operation replace + iosxr_bgp: &af_net_rplc + operation: replace + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: unicast + networks: + - prefix: 198.51.100.80 + masklen: 28 + + - prefix: 192.0.2.64 + masklen: 27 + + - prefix: 203.0.113.192 + masklen: 27 + + - afi: ipv4 + safi: multicast + networks: + - prefix: 198.51.100.64 + masklen: 28 + register: result + +- assert: + that: + - 'result.changed == true' + - '"router bgp 64496" in result.commands' + - '"address-family ipv4 unicast" in result.commands' + - '"network 198.51.100.80/28" in result.commands' + - '"network 203.0.113.192/27" in result.commands' + - '"no network 198.51.100.48/28" in result.commands' + - '"no network 203.0.113.160/27" in result.commands' + +- name: Configure networks under address family with operation replace (idempotent) + iosxr_bgp: *af_net_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Override all the exisiting BGP config + iosxr_bgp: + operation: override + config: + bgp_as: 64497 + router_id: 192.0.2.10 + log_neighbor_changes: True + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64497' in result.commands" + - "'bgp router-id 192.0.2.10' in result.commands" + - "'bgp log neighbor changes detail' in result.commands" + +- name: Teardown + iosxr_bgp: &rm + operation: delete + register: result + +- assert: + that: + - 'result.changed == true' + - "'no router bgp 64497' in result.commands" + +- name: Teardown again (idempotent) + iosxr_bgp: *rm + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END iosxr cli/iosxr_bgp.yaml on connection={{ ansible_connection }}"