From 9365c0f468071d704130d270117cf146cdde9d74 Mon Sep 17 00:00:00 2001 From: Nilashish Chakraborty Date: Tue, 12 Mar 2019 17:31:58 +0530 Subject: [PATCH] New module for BGP configuration management in Arista EOS (#52722) * New module for BGP in EOS Signed-off-by: NilashishC * Add function to validate input Signed-off-by: NilashishC * Fix line indentation Signed-off-by: NilashishC * Add integration tests Signed-off-by: NilashishC * Fix CI Signed-off-by: NilashishC * Fix sanity test failure Signed-off-by: NilashishC * Remove unused code Signed-off-by: NilashishC --- .../network/eos/providers/__init__.py | 0 .../network/eos/providers/cli/__init__.py | 0 .../eos/providers/cli/config/__init__.py | 0 .../eos/providers/cli/config/bgp/__init__.py | 0 .../cli/config/bgp/address_family.py | 130 ++++++ .../eos/providers/cli/config/bgp/neighbors.py | 173 ++++++++ .../eos/providers/cli/config/bgp/process.py | 162 +++++++ .../network/eos/providers/module.py | 62 +++ .../network/eos/providers/providers.py | 120 ++++++ lib/ansible/modules/network/eos/eos_bgp.py | 394 ++++++++++++++++++ .../targets/eos_bgp/defaults/main.yaml | 3 + .../targets/eos_bgp/meta/main.yaml | 2 + .../targets/eos_bgp/tasks/cli.yaml | 16 + .../targets/eos_bgp/tasks/main.yaml | 2 + .../targets/eos_bgp/tests/cli/basic.yaml | 372 +++++++++++++++++ .../network/eos/fixtures/eos_bgp_config.cfg | 24 ++ .../units/modules/network/eos/test_eos_bgp.py | 197 +++++++++ 17 files changed, 1657 insertions(+) create mode 100644 lib/ansible/module_utils/network/eos/providers/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/config/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/config/bgp/__init__.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/config/bgp/address_family.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/config/bgp/neighbors.py create mode 100644 lib/ansible/module_utils/network/eos/providers/cli/config/bgp/process.py create mode 100644 lib/ansible/module_utils/network/eos/providers/module.py create mode 100644 lib/ansible/module_utils/network/eos/providers/providers.py create mode 100644 lib/ansible/modules/network/eos/eos_bgp.py create mode 100644 test/integration/targets/eos_bgp/defaults/main.yaml create mode 100644 test/integration/targets/eos_bgp/meta/main.yaml create mode 100644 test/integration/targets/eos_bgp/tasks/cli.yaml create mode 100644 test/integration/targets/eos_bgp/tasks/main.yaml create mode 100644 test/integration/targets/eos_bgp/tests/cli/basic.yaml create mode 100644 test/units/modules/network/eos/fixtures/eos_bgp_config.cfg create mode 100644 test/units/modules/network/eos/test_eos_bgp.py diff --git a/lib/ansible/module_utils/network/eos/providers/__init__.py b/lib/ansible/module_utils/network/eos/providers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/eos/providers/cli/__init__.py b/lib/ansible/module_utils/network/eos/providers/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/eos/providers/cli/config/__init__.py b/lib/ansible/module_utils/network/eos/providers/cli/config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/__init__.py b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/address_family.py b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/address_family.py new file mode 100644 index 0000000000..d8e15c1e7f --- /dev/null +++ b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/address_family.py @@ -0,0 +1,130 @@ +# +# (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.eos.providers.providers import CliProvider +from ansible.module_utils.network.eos.providers.cli.config.bgp.neighbors import AFNeighbors +from ansible.module_utils.common.network import to_netmask + + +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' % item['afi'] + context_commands = list() + + if config: + context_path = [router_context, context] + context_config = self.get_config_context(config, context_path, indent=2) + + 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 self.params['operation'] == 'replace': + 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_auto_summary(self, item, config=None): + cmd = 'auto-summary' + if item['auto_summary'] is False: + cmd = 'no %s' % cmd + if not config or cmd not in config: + return cmd + + def _render_synchronization(self, item, config=None): + cmd = 'synchronization' + if item['synchronization'] is False: + cmd = 'no %s' % cmd + if not config or cmd not in config: + return cmd + + 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-map %s' % entry['route_map'] + + if not config or cmd not in config: + commands.append(cmd) + + if self.params['operation'] == 'replace': + if config: + 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['route_map']: + cmd += ' route-map %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 + + def _render_neighbors(self, item, config): + """ generate bgp neighbor configuration + """ + return AFNeighbors(self.params).render(config, nbr_list=item['neighbors']) diff --git a/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/neighbors.py b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/neighbors.py new file mode 100644 index 0000000000..329916b69f --- /dev/null +++ b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/neighbors.py @@ -0,0 +1,173 @@ +# +# (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.eos.providers.providers import CliProvider + + +class Neighbors(CliProvider): + + def render(self, config=None, nbr_list=None): + commands = list() + safe_list = list() + if not nbr_list: + nbr_list = self.get_value('config.neighbors') + + for item in nbr_list: + neighbor_commands = list() + context = 'neighbor %s' % item['neighbor'] + cmd = '%s remote-as %s' % (context, item['remote_as']) + + if not config or cmd not in config: + neighbor_commands.append(cmd) + + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, '_render_%s' % key, None) + if meth: + resp = meth(item, config) + if resp: + neighbor_commands.extend(to_list(resp)) + + commands.extend(neighbor_commands) + safe_list.append(context) + + if self.params['operation'] == 'replace': + 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_description(self, item, config=None): + cmd = 'neighbor %s description %s' % (item['neighbor'], item['description']) + if not config or cmd not in config: + return cmd + + def _render_enabled(self, item, config=None): + cmd = 'neighbor %s shutdown' % item['neighbor'] + if item['enabled'] is True: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_update_source(self, item, config=None): + cmd = 'neighbor %s update-source %s' % (item['neighbor'], item['update_source']) + if not config or cmd not in config: + return cmd + + def _render_password(self, item, config=None): + cmd = 'neighbor %s password %s' % (item['neighbor'], item['password']) + if not config or cmd not in config: + return cmd + + def _render_ebgp_multihop(self, item, config=None): + cmd = 'neighbor %s ebgp-multihop %s' % (item['neighbor'], item['ebgp_multihop']) + if not config or cmd not in config: + return cmd + + def _render_peer_group(self, item, config=None): + cmd = 'neighbor %s peer-group %s' % (item['neighbor'], item['peer_group']) + if not config or cmd not in config: + return cmd + + def _render_route_reflector_client(self, item, config=None): + cmd = 'neighbor %s route-reflector-client' % item['neighbor'] + if item['route_reflector_client'] is False: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_maximum_prefix(self, item, config=None): + cmd = 'neighbor %s maximum-routes %s' % (item['neighbor'], item['maximum_prefix']) + if not config or cmd not in config: + return cmd + + def _render_remove_private_as(self, item, config=None): + cmd = 'neighbor %s remove-private-AS' % item['neighbor'] + if item['remove_private_as'] is False: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif 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'] + neighbor = item['neighbor'] + + if keepalive and holdtime: + cmd = 'neighbor %s timers %s %s' % (neighbor, keepalive, holdtime) + if not config or cmd not in config: + return cmd + + +class AFNeighbors(CliProvider): + + def render(self, config=None, nbr_list=None): + commands = list() + if not nbr_list: + return + + for item in nbr_list: + neighbor_commands = list() + for key, value in iteritems(item): + if value is not None: + meth = getattr(self, '_render_%s' % key, None) + if meth: + resp = meth(item, config) + if resp: + neighbor_commands.extend(to_list(resp)) + + commands.extend(neighbor_commands) + + return commands + + def _render_activate(self, item, config=None): + cmd = 'neighbor %s activate' % item['neighbor'] + if item['activate'] is False: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_default_originate(self, item, config=None): + cmd = 'neighbor %s default-originate' % item['neighbor'] + if item['activate'] is False: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_graceful_restart(self, item, config=None): + cmd = 'neighbor %s graceful-restart' % item['neighbor'] + if item['activate'] is False: + if not config or cmd in config: + cmd = 'no %s' % cmd + return cmd + elif not config or cmd not in config: + return cmd + + def _render_weight(self, item, config=None): + cmd = 'neighbor %s weight %s' % (item['neighbor'], item['weight']) + if not config or cmd not in config: + return cmd diff --git a/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/process.py b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/process.py new file mode 100644 index 0000000000..a5302b2c37 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/providers/cli/config/bgp/process.py @@ -0,0 +1,162 @@ +# +# (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.eos.providers.providers import register_provider +from ansible.module_utils.network.eos.providers.providers import CliProvider +from ansible.module_utils.network.eos.providers.cli.config.bgp.neighbors import Neighbors +from ansible.module_utils.network.eos.providers.cli.config.bgp.address_family import AddressFamily + +REDISTRIBUTE_PROTOCOLS = frozenset(['ospf', 'ospf3', 'rip', 'isis', 'static', 'connected']) + + +@register_provider('eos', 'eos_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) + 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: + self._validate_input(config) + if operation == 'replace': + if existing_as and int(existing_as) != self.get_value('config.bgp_as'): + commands.append('no router bgp %s' % existing_as) + config = None + + elif operation == 'override': + if existing_as: + commands.append('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 = '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 cmd + elif log_neighbor_changes is False: + if config and cmd in config: + return 'no %s' % cmd + + def _render_networks(self, config=None): + commands = list() + safe_list = list() + + for entry in self.get_value('config.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-map %s' % entry['route_map'] + + if not config or cmd not in config: + commands.append(cmd) + + if self.params['operation'] == 'replace': + if config: + 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, config=None): + commands = list() + safe_list = list() + + for entry in self.get_value('config.redistribute'): + option = entry['protocol'] + + cmd = 'redistribute %s' % entry['protocol'] + + if entry['route_map']: + cmd += ' route-map %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 + + 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) + + def _validate_input(self, config): + def device_has_AF(config): + return re.search(r'address-family (?:.*)', config) + + address_family = self.get_value('config.address_family') + root_networks = self.get_value('config.networks') + operation = self.params['operation'] + + if operation == 'replace' and root_networks: + if address_family: + for item in address_family: + if item['networks']: + raise ValueError('operation is replace but provided both root level networks and networks under %s address family' + % item['afi']) + + if config and device_has_AF(config): + raise ValueError('operation is replace and device has one or more address family activated but root level network(s) provided') diff --git a/lib/ansible/module_utils/network/eos/providers/module.py b/lib/ansible/module_utils/network/eos/providers/module.py new file mode 100644 index 0000000000..9f259ebb46 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/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.eos.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/eos/providers/providers.py b/lib/ansible/module_utils/network/eos/providers/providers.py new file mode 100644 index 0000000000..be429dc0f8 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/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=2): + 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/eos/eos_bgp.py b/lib/ansible/modules/network/eos/eos_bgp.py new file mode 100644 index 0000000000..b60e42a15f --- /dev/null +++ b/lib/ansible/modules/network/eos/eos_bgp.py @@ -0,0 +1,394 @@ +#!/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: eos_bgp +version_added: "2.8" +author: "Nilashish Chakraborty (@nilashishc)" +short_description: Configure global BGP protocol settings on Arista EOS. +description: + - This module provides configuration management of global BGP parameters + on Arista EOS devices. +notes: + - Tested against Arista vEOS Version 4.15.9M. +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. + description: + description: + - Neighbor specific description. + ebgp_multihop: + description: + - Specifies the maximum hop count for EBGP neighbors not on directly connected networks. + - The range is from 1 to 255. + type: int + peer_group: + description: + - Name of the peer group that the neighbor is a member of. + timers: + description: + - Specifies BGP neighbor timer related configurations. + suboptions: + keepalive: + description: + - Frequency (in seconds) with which the device sends keepalive messages to its peer. + - The range is from 0 to 3600. + type: int + required: True + holdtime: + description: + - Interval (in seconds) after not receiving a keepalive message that device declares a peer dead. + - The range is from 3 to 7200. + - Setting this value to 0 will not send keep-alives (hold forever). + type: int + required: True + route_reflector_client: + description: + - Specify a neighbor as a route reflector client. + type: bool + remove_private_as: + description: + - Remove the private AS number from outbound updates. + type: bool + enabled: + description: + - Administratively shutdown or enable a neighbor. + maximum_prefix: + description: + - Maximum number of prefixes to accept from this peer. + - The range is from 0 to 4294967294. + type: int + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + required: True + 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 networks option under address_family. + - For operation replace, if the device already has an address family activated, this option is not allowed. + suboptions: + prefix: + 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. + address_family: + description: + - Specifies BGP address family related configurations. + suboptions: + afi: + description: + - Type of address family to configure. + choices: + - ipv4 + - ipv6 + required: True + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + required: True + 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: + prefix: + 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. + neighbors: + description: + - Specifies BGP neighbor related configurations in Address Family configuration mode. + suboptions: + neighbor: + description: + - Neighbor router address. + required: True + activate: + description: + - Enable the Address Family for this Neighbor. + type: bool + default_originate: + description: + - Originate default route to this neighbor. + type: bool + graceful_restart: + description: + - Enable/disable graceful restart mode for this neighbor. + type: bool + weight: + description: + - Assign weight for routes learnt from this neighbor. + - The range is from 0 to 65535 + type: int + 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 64496 + eos_bgp: + config: + bgp_as: 64496 + router_id: 192.0.2.1 + log_neighbor_changes: True + neighbors: + - neighbor: 203.0.113.5 + remote_as: 64511 + timers: + keepalive: 300 + holdtime: 360 + - neighbor: 198.51.100.2 + remote_as: 64498 + networks: + - prefix: 198.51.100.0 + route_map: RMAP_1 + - prefix: 192.0.2.0 + masklen: 23 + address_family: + - afi: ipv4 + safi: unicast + redistribute: + - protocol: isis + route_map: RMAP_1 + operation: merge + +- name: Configure BGP neighbors + eos_bgp: + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + description: IBGP_NBR_1 + ebgp_multihop: 100 + timers: + keepalive: 300 + holdtime: 360 + + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + ebgp_multihop: 150 + operation: merge + +- name: Configure root-level networks for BGP + eos_bgp: + config: + bgp_as: 64496 + networks: + - prefix: 203.0.113.0 + masklen: 27 + route_map: RMAP_1 + + - prefix: 203.0.113.32 + masklen: 27 + route_map: RMAP_2 + operation: merge + +- name: Configure BGP neighbors under address family mode + eos_bgp: + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + neighbors: + - neighbor: 203.0.113.10 + activate: yes + default_originate: True + + - neighbor: 192.0.2.15 + activate: yes + graceful_restart: True + operation: merge + +- name: remove bgp as 64496 from config + eos_bgp: + config: + bgp_as: 64496 + operation: delete +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - router bgp 64496 + - bgp router-id 192.0.2.1 + - bgp log-neighbor-changes + - neighbor 203.0.113.5 remote-as 64511 + - neighbor 203.0.113.5 timers 300 360 + - neighbor 198.51.100.2 remote-as 64498 + - network 198.51.100.0 route-map RMAP_1 + - network 192.0.2.0 mask 255.255.254.0 + - address-family ipv4 + - redistribute isis route-map RMAP_1 + - exit-address-family +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.network.eos.providers.module import NetworkModule +from ansible.module_utils.network.eos.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'), + 'route_map': dict(), + } + + redistribute_spec = { + 'protocol': dict(choices=REDISTRIBUTE_PROTOCOLS, required=True), + 'route_map': dict(), + } + + timer_spec = { + 'keepalive': dict(type='int', required=True), + 'holdtime': dict(type='int', required=True), + } + + 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(), + 'ebgp_multihop': dict(type='int'), + 'timers': dict(type='dict', options=timer_spec), + 'peer_group': dict(), + 'maximum_prefix': dict(type='int'), + 'route_reflector_client': dict(type='int'), + 'remove_private_as': dict(type='bool') + } + + af_neighbor_spec = { + 'neighbor': dict(required=True), + 'activate': dict(type='bool'), + 'default_originate': dict(type='bool'), + 'graceful_restart': dict(type='bool'), + 'weight': dict(type='int'), + } + + address_family_spec = { + 'afi': dict(choices=['ipv4', 'ipv6'], required=True), + 'networks': dict(type='list', elements='dict', options=network_spec), + 'redistribute': dict(type='list', elements='dict', options=redistribute_spec), + 'neighbors': dict(type='list', elements='dict', options=af_neighbor_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), + 'redistribute': dict(type='list', elements='dict', options=redistribute_spec), + 'networks': dict(type='list', elements='dict', options=network_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='| section 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/eos_bgp/defaults/main.yaml b/test/integration/targets/eos_bgp/defaults/main.yaml new file mode 100644 index 0000000000..9ef5ba5165 --- /dev/null +++ b/test/integration/targets/eos_bgp/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/eos_bgp/meta/main.yaml b/test/integration/targets/eos_bgp/meta/main.yaml new file mode 100644 index 0000000000..e5c8cd02f0 --- /dev/null +++ b/test/integration/targets/eos_bgp/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_eos_tests diff --git a/test/integration/targets/eos_bgp/tasks/cli.yaml b/test/integration/targets/eos_bgp/tasks/cli.yaml new file mode 100644 index 0000000000..87a42971bb --- /dev/null +++ b/test/integration/targets/eos_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 cases (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_bgp/tasks/main.yaml b/test/integration/targets/eos_bgp/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/eos_bgp/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/eos_bgp/tests/cli/basic.yaml b/test/integration/targets/eos_bgp/tests/cli/basic.yaml new file mode 100644 index 0000000000..0864152d61 --- /dev/null +++ b/test/integration/targets/eos_bgp/tests/cli/basic.yaml @@ -0,0 +1,372 @@ +- debug: msg="START eos cli/eos_bgp.yaml on connection={{ ansible_connection }}" + +- name: Clear existing BGP config + eos_bgp: + operation: delete + ignore_errors: yes + +- name: Configure BGP with AS 64496 and a router-id + eos_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" + - "'router-id 192.0.2.2' in result.commands" + +- name: Configure BGP with AS 64496 and a router-id (idempotent) + eos_bgp: *config + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors + eos_bgp: &nbr + operation: merge + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + description: IBGP_NBR_1 + ebgp_multihop: 100 + timers: + keepalive: 300 + holdtime: 360 + + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + ebgp_multihop: 150 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'neighbor 192.0.2.10 remote-as 64496' in result.commands" + - "'neighbor 192.0.2.10 description IBGP_NBR_1' in result.commands" + - "'neighbor 192.0.2.10 ebgp-multihop 100' in result.commands" + - "'neighbor 192.0.2.10 timers 300 360' in result.commands" + - "'neighbor 192.0.2.15 remote-as 64496' in result.commands" + - "'neighbor 192.0.2.15 description IBGP_NBR_2' in result.commands" + - "'neighbor 192.0.2.15 ebgp-multihop 150' in result.commands" + +- name: Configure BGP neighbors (idempotent) + eos_bgp: *nbr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors with operation replace + eos_bgp: &nbr_rplc + operation: replace + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + ebgp_multihop: 150 + + - neighbor: 203.0.113.10 + remote_as: 64511 + description: EBGP_NBR_1 + register: result + +- assert: + that: + - 'result.changed == true' + - "'neighbor 203.0.113.10 remote-as 64511' in result.commands" + - "'neighbor 203.0.113.10 description EBGP_NBR_1' in result.commands" + - "'no neighbor 192.0.2.10' in result.commands" + +- name: Configure BGP neighbors with operation replace (idempotent) + eos_bgp: *nbr_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure root-level networks for BGP + eos_bgp: &net + operation: merge + config: + bgp_as: 64496 + networks: + - prefix: 203.0.113.0 + masklen: 27 + route_map: RMAP_1 + + - prefix: 203.0.113.32 + masklen: 27 + route_map: RMAP_2 + register: result + +- assert: + that: + - 'result.changed == True' + - "'router bgp 64496' in result.commands" + - "'network 203.0.113.0/27 route-map RMAP_1' in result.commands" + - "'network 203.0.113.32/27 route-map RMAP_2' in result.commands" + +- name: Configure root-level networks for BGP (idempotent) + eos_bgp: *net + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure root-level networks for BGP with operation replace + eos_bgp: &net_rplc + operation: replace + config: + bgp_as: 64496 + networks: + - prefix: 203.0.113.0 + masklen: 27 + route_map: RMAP_1 + + - prefix: 198.51.100.16 + masklen: 28 + register: result + +- assert: + that: + - 'result.changed == True' + - "'router bgp 64496' in result.commands" + - "'network 198.51.100.16/28' in result.commands" + - "'no network 203.0.113.32/27' in result.commands" + +- name: Configure root-level networks for BGP with operation replace (idempotent) + eos_bgp: *net_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP route redistribute information + eos_bgp: &rdr + operation: merge + config: + bgp_as: 64496 + redistribute: + - protocol: ospf + route_map: RMAP_1 + + - protocol: rip + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'redistribute ospf route-map RMAP_1' in result.commands" + - "'redistribute rip' in result.commands" + +- name: Configure BGP route redistribute information (idempotent) + eos_bgp: *rdr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP route redistribute information with operation replace + eos_bgp: &rdr_rplc + operation: replace + config: + bgp_as: 64496 + redistribute: + - protocol: ospf + route_map: RMAP_1 + + - protocol: static + route_map: RMAP_2 + register: result + +- assert: + that: + - 'result.changed == true' + - "'redistribute static route-map RMAP_2' in result.commands" + - "'no redistribute rip' in result.commands" + +- name: Configure BGP route redistribute information with operation replace (idempotent) + eos_bgp: *rdr_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors under address family mode + eos_bgp: &af_nbr + operation: merge + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + neighbors: + - neighbor: 203.0.113.10 + activate: yes + default_originate: True + + - neighbor: 192.0.2.15 + activate: yes + graceful_restart: True + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4' in result.commands" + - "'neighbor 203.0.113.10 activate' in result.commands" + - "'neighbor 203.0.113.10 default-originate' in result.commands" + - "'neighbor 192.0.2.15 activate' in result.commands" + - "'neighbor 192.0.2.15 graceful-restart' in result.commands" + +- name: Configure BGP neighbors under address family mode (idempotent) + eos_bgp: *af_nbr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family + eos_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 + route_map: RMAP_2 + + - afi: ipv6 + networks: + - prefix: "2001:db8::" + masklen: 33 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4' in result.commands" + - "'network 198.51.100.48/28 route-map RMAP_1' in result.commands" + - "'network 192.0.2.64/27' in result.commands" + - "'network 203.0.113.160/27 route-map RMAP_2' in result.commands" + - "'address-family ipv6' in result.commands" + - "'network 2001:db8::/33' in result.commands" + +- name: Configure networks under address family (idempotent) + eos_bgp: *af_net + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family with operation replace + eos_bgp: &af_net_rplc + operation: replace + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + networks: + - prefix: 198.51.100.80 + masklen: 28 + + - prefix: 192.0.2.64 + masklen: 27 + + - prefix: 203.0.113.192 + masklen: 27 + + - afi: ipv6 + networks: + - prefix: "2001:db8:1000::" + masklen: 37 + register: result + +- assert: + that: + - 'result.changed == true' + - '"router bgp 64496" in result.commands' + - '"address-family ipv4" 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' + - '"address-family ipv6" in result.commands' + - '"network 2001:db8:1000::/37" in result.commands' + - '"no network 2001:db8::/33" in result.commands' + +- name: Configure networks under address family with operation replace (idempotent) + eos_bgp: *af_net_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Override all the exisiting BGP config + eos_bgp: + operation: override + config: + bgp_as: 64497 + router_id: 192.0.2.10 + log_neighbor_changes: True + register: result + +- assert: + that: + - 'result.changed == true' + - "'no router bgp 64496' in result.commands" + - "'router bgp 64497' in result.commands" + - "'router-id 192.0.2.10' in result.commands" + - "'bgp log-neighbor-changes' in result.commands" + +- name: Teardown + eos_bgp: &rm + operation: delete + register: result + +- assert: + that: + - 'result.changed == true' + - "'no router bgp 64497' in result.commands" + +- name: Teardown again (idempotent) + eos_bgp: *rm + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END eos cli/eos_bgp.yaml on connection={{ ansible_connection }}" diff --git a/test/units/modules/network/eos/fixtures/eos_bgp_config.cfg b/test/units/modules/network/eos/fixtures/eos_bgp_config.cfg new file mode 100644 index 0000000000..3df62417d1 --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_bgp_config.cfg @@ -0,0 +1,24 @@ +router bgp 64496 + bgp router-id 192.0.2.1 + bgp log-neighbor-changes + neighbor 198.51.100.102 remote-as 64498 + neighbor 198.51.100.102 timers 300 360 + neighbor 192.0.2.111 remote-as 64496 + neighbor 192.0.2.111 update-source Ethernet1 + neighbor 203.0.113.5 remote-as 64511 + neighbor 203.0.113.5 maximum-routes 500 + redistribute ospf route-map RMAP_1 + address-family ipv4 + neighbor 198.51.100.102 activate + neighbor 198.51.100.102 graceful-restart + neighbor 198.51.100.102 default-originate + neighbor 198.51.100.102 weight 25 + neighbor 192.0.2.111 activate + neighbor 192.0.2.111 default-originate + network 192.0.2.0/27 route-map RMAP_1 + network 198.51.100.0/24 route-map RMAP_2 + ! + address-family ipv6 + network 2001:db8:8000::/34 + network 2001:db8:c000::/34 + diff --git a/test/units/modules/network/eos/test_eos_bgp.py b/test/units/modules/network/eos/test_eos_bgp.py new file mode 100644 index 0000000000..d39f80feeb --- /dev/null +++ b/test/units/modules/network/eos/test_eos_bgp.py @@ -0,0 +1,197 @@ +# +# (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 ansible.module_utils.network.eos.providers.cli.config.bgp.process import Provider +from ansible.modules.network.eos import eos_bgp +from .eos_module import TestEosModule, load_fixture + + +class TestFrrBgpModule(TestEosModule): + module = eos_bgp + + def setUp(self): + super(TestFrrBgpModule, self).setUp() + self._bgp_config = load_fixture('eos_bgp_config.cfg') + + def test_eos_bgp(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, router_id='192.0.2.2', networks=None, + address_family=None), operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, ['router bgp 64496', 'router-id 192.0.2.2', 'exit']) + + def test_eos_bgp_idempotent(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, router_id='192.0.2.1', + networks=None, address_family=None), operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_remove(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, networks=None, address_family=None), operation='delete')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, ['no router bgp 64496']) + + def test_eos_bgp_neighbor(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, neighbors=[dict(neighbor='198.51.100.12', remote_as=64498)], + networks=None, address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, ['router bgp 64496', 'neighbor 198.51.100.12 remote-as 64498', 'exit']) + + def test_eos_bgp_neighbor_idempotent(self): + neighbors = [dict(neighbor='198.51.100.102', remote_as=64498, timers=dict(keepalive=300, holdtime=360)), + dict(neighbor='203.0.113.5', remote_as=64511, maximum_prefix=500)] + obj = Provider(params=dict(config=dict(bgp_as=64496, neighbors=neighbors, networks=None, address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_network(self): + obj = Provider( + params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='203.0.113.0', masklen=24, route_map='RMAP_1')], + address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(sorted(commands), sorted(['router bgp 64496', 'network 203.0.113.0/24 route-map RMAP_1', 'exit'])) + + def test_eos_bgp_network_idempotent(self): + obj = Provider( + params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='192.0.2.0', masklen=27, route_map='RMAP_1'), + dict(prefix='198.51.100.0', masklen=24, route_map='RMAP_2')], + address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_redistribute(self): + rd_1 = dict(protocol='rip', route_map='RMAP_1') + + config = dict(bgp_as=64496, redistribute=[rd_1], networks=None, address_family=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + cmd = ['router bgp 64496', 'redistribute rip route-map RMAP_1', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_eos_bgp_redistribute_idempotent(self): + rd_1 = dict(protocol='ospf', route_map='RMAP_1') + config = dict(bgp_as=64496, redistribute=[rd_1], networks=None, address_family=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_address_family_neighbors(self): + af_nbr_1 = dict(neighbor='198.51.100.104', default_originate=True, activate=True) + af_nbr_2 = dict(neighbor='198.51.100.105', activate=True, weight=30, graceful_restart=True) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', neighbors=[af_nbr_1, af_nbr_2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + cmd = ['router bgp 64496', 'address-family ipv4', 'neighbor 198.51.100.104 activate', + 'neighbor 198.51.100.104 default-originate', 'neighbor 198.51.100.105 weight 30', + 'neighbor 198.51.100.105 activate', 'neighbor 198.51.100.105 graceful-restart', 'exit', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_eos_bgp_address_family_neighbors_idempotent(self): + af_nbr_1 = dict(neighbor='198.51.100.102', activate=True, graceful_restart=True, default_originate=True, weight=25) + af_nbr_2 = dict(neighbor='192.0.2.111', activate=True, default_originate=True) + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', neighbors=[af_nbr_1, af_nbr_2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_address_family_networks(self): + net = dict(prefix='203.0.113.128', masklen=26, route_map='RMAP_1') + net2 = dict(prefix='203.0.113.192', masklen=26, route_map='RMAP_2') + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', networks=[net, net2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + cmd = ['router bgp 64496', 'address-family ipv4', 'network 203.0.113.128/26 route-map RMAP_1', + 'network 203.0.113.192/26 route-map RMAP_2', 'exit', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_eos_bgp_address_family_networks_idempotent(self): + net = dict(prefix='2001:db8:8000::', masklen=34, route_map=None) + net2 = dict(prefix='2001:db8:c000::', masklen=34, route_map=None) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv6', networks=[net, net2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_eos_bgp_operation_override(self): + net_1 = dict(prefix='2001:0db8:0800::', masklen=38, route_map='RMAP_1') + net_2 = dict(prefix='2001:0db8:1c00::', masklen=38, route_map='RMAP_2') + nbr_1 = dict(neighbor='203.0.113.111', remote_as=64511, update_source='Ethernet2') + nbr_2 = dict(neighbor='203.0.113.120', remote_as=64511, timers=dict(keepalive=300, holdtime=360)) + af_nbr_1 = dict(neighbor='203.0.113.111', activate=True) + af_nbr_2 = dict(neighbor='203.0.113.120', activate=True, default_originate=True) + + af_1 = dict(afi='ipv4', neighbors=[af_nbr_1, af_nbr_2]) + af_2 = dict(afi='ipv6', networks=[net_1, net_2]) + config = dict(bgp_as=64496, neighbors=[nbr_1, nbr_2], address_family=[af_1, af_2], + networks=None) + + obj = Provider(params=dict(config=config, operation='override')) + commands = obj.render(self._bgp_config) + + cmd = ['no router bgp 64496', 'router bgp 64496', 'neighbor 203.0.113.111 remote-as 64511', + 'neighbor 203.0.113.111 update-source Ethernet2', 'neighbor 203.0.113.120 remote-as 64511', + 'neighbor 203.0.113.120 timers 300 360', 'address-family ipv4', + 'neighbor 203.0.113.111 activate', 'neighbor 203.0.113.120 default-originate', 'neighbor 203.0.113.120 activate', + 'exit', 'address-family ipv6', 'network 2001:0db8:0800::/38 route-map RMAP_1', + 'network 2001:0db8:1c00::/38 route-map RMAP_2', + 'exit', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_eos_bgp_operation_replace(self): + net = dict(prefix='203.0.113.0', masklen=27, route_map='RMAP_1') + net2 = dict(prefix='192.0.2.32', masklen=29, route_map='RMAP_2') + net_3 = dict(prefix='2001:db8:8000::', masklen=34, route_map=None) + net_4 = dict(prefix='2001:db8:c000::', masklen=34, route_map=None) + + af_1 = dict(afi='ipv4', networks=[net, net2]) + af_2 = dict(afi='ipv6', networks=[net_3, net_4]) + + config = dict(bgp_as=64496, address_family=[af_1, af_2], networks=None) + obj = Provider(params=dict(config=config, operation='replace')) + commands = obj.render(self._bgp_config) + + cmd = ['router bgp 64496', 'address-family ipv4', 'network 203.0.113.0/27 route-map RMAP_1', + 'network 192.0.2.32/29 route-map RMAP_2', 'no network 192.0.2.0/27', 'no network 198.51.100.0/24', + 'exit', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_eos_bgp_operation_replace_with_new_as(self): + nbr = dict(neighbor='203.0.113.124', remote_as=64496, update_source='Ethernet3') + + config = dict(bgp_as=64497, neighbors=[nbr], networks=None, address_family=None) + obj = Provider(params=dict(config=config, operation='replace')) + commands = obj.render(self._bgp_config) + + cmd = ['no router bgp 64496', 'router bgp 64497', 'neighbor 203.0.113.124 remote-as 64496', + 'neighbor 203.0.113.124 update-source Ethernet3', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd))