diff --git a/lib/ansible/module_utils/network/frr/providers/__init__.py b/lib/ansible/module_utils/network/frr/providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/frr/providers/cli/__init__.py b/lib/ansible/module_utils/network/frr/providers/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/frr/providers/cli/config/__init__.py b/lib/ansible/module_utils/network/frr/providers/cli/config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/frr/providers/cli/config/base.py b/lib/ansible/module_utils/network/frr/providers/cli/config/base.py new file mode 100644 index 00000000000..eade249a09c --- /dev/null +++ b/lib/ansible/module_utils/network/frr/providers/cli/config/base.py @@ -0,0 +1,77 @@ +# +# (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.six import iteritems +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.common.config import NetworkConfig + + +class ConfigBase(object): + + argument_spec = {} + + mutually_exclusive = [] + + identifier = () + + def __init__(self, **kwargs): + self.values = {} + self._rendered_configuration = {} + self.active_configuration = None + + for item in self.identifier: + self.values[item] = kwargs.pop(item) + + for key, value in iteritems(kwargs): + if key in self.argument_spec: + setattr(self, key, value) + + for key, value in iteritems(self.argument_spec): + if value.get('default'): + if not getattr(self, key, None): + setattr(self, key, value.get('default')) + + def __getattr__(self, key): + if key in self.argument_spec: + return self.values.get(key) + + def __setattr__(self, key, value): + if key in self.argument_spec: + if key in self.identifier: + raise TypeError('cannot set value') + elif value is not None: + self.values[key] = value + else: + super(ConfigBase, self).__setattr__(key, value) + + def context_config(self, cmd): + if 'context' not in self._rendered_configuration: + self._rendered_configuration['context'] = list() + self._rendered_configuration['context'].extend(to_list(cmd)) + + def global_config(self, cmd): + if 'global' not in self._rendered_configuration: + self._rendered_configuration['global'] = list() + self._rendered_configuration['global'].extend(to_list(cmd)) + + def get_rendered_configuration(self): + config = list() + for section in ('context', 'global'): + config.extend(self._rendered_configuration.get(section, [])) + return config + + def set_active_configuration(self, config): + self.active_configuration = config + + def render(self, config=None): + raise NotImplementedError + + def get_section(self, config, section): + if config is not None: + netcfg = NetworkConfig(indent=1, contents=config) + try: + config = netcfg.get_block_config(to_list(section)) + except ValueError: + config = None + return config diff --git a/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/__init__.py b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/address_family.py b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/address_family.py new file mode 100644 index 00000000000..7627cc3a7cb --- /dev/null +++ b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/address_family.py @@ -0,0 +1,136 @@ +# +# (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.frr.providers.providers import CliProvider +from ansible.module_utils.network.frr.providers.cli.config.bgp.neighbors import AFNeighbors + + +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-address-family') + + 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['id'] and entry['protocol'] in ('ospf', 'table'): + cmd += ' %s' % entry['id'] + option += ' %s' % entry['id'] + + if entry['metric']: + cmd += ' metric %s' % entry['metric'] + + 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/frr/providers/cli/config/bgp/neighbors.py b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/neighbors.py new file mode 100644 index 00000000000..bd3267f211c --- /dev/null +++ b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/neighbors.py @@ -0,0 +1,183 @@ +# +# (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.frr.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_advertisement_interval(self, item, config=None): + cmd = 'neighbor %s advertisement-interval %s' % (item['neighbor'], item['advertisement_interval']) + if not config or cmd not in config: + return cmd + + def _render_local_as(self, item, config=None): + cmd = 'neighbor %s local-as %s' % (item['neighbor'], item['local_as']) + if not config or cmd not in config: + return cmd + + def _render_port(self, item, config=None): + cmd = 'neighbor %s port %s' % (item['neighbor'], item['port']) + if not config or cmd not in config: + return cmd + + 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: + cmd = 'no %s' % cmd + if 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_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 + else: + raise ValueError("required both options for timers: keepalive and holdtime") + + +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_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_route_server_client(self, item, config=None): + cmd = 'neighbor %s route-server-client' % item['neighbor'] + if item['route_server_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_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_next_hop_self(self, item, config=None): + cmd = 'neighbor %s activate' % item['neighbor'] + if item['next_hop_self'] 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_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_maximum_prefix(self, item, config=None): + cmd = 'neighbor %s maximum-prefix %s' % (item['neighbor'], item['maximum_prefix']) + if not config or cmd not in config: + return cmd diff --git a/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/process.py b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/process.py new file mode 100644 index 00000000000..bacdf6ee6b0 --- /dev/null +++ b/lib/ansible/module_utils/network/frr/providers/cli/config/bgp/process.py @@ -0,0 +1,137 @@ +# +# (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.frr.providers.providers import register_provider +from ansible.module_utils.network.frr.providers.providers import CliProvider +from ansible.module_utils.network.frr.providers.cli.config.bgp.neighbors import Neighbors +from ansible.module_utils.network.frr.providers.cli.config.bgp.address_family import AddressFamily + +REDISTRIBUTE_PROTOCOLS = frozenset(['ospf', 'ospf6', 'eigrp', 'isis', 'table', + 'static', 'connected', 'sharp', 'nhrp', 'kernel', 'babel', 'rip']) + + +@register_provider('frr', 'frr_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: + 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 = '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 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_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 root_networks and operation == 'replace': + 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 %s address family' + % (item['afi'], item['safi'])) + 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/frr/providers/module.py b/lib/ansible/module_utils/network/frr/providers/module.py new file mode 100644 index 00000000000..d1dfa6c0691 --- /dev/null +++ b/lib/ansible/module_utils/network/frr/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.frr.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/frr/providers/providers.py b/lib/ansible/module_utils/network/frr/providers/providers.py new file mode 100644 index 00000000000..a466b033d94 --- /dev/null +++ b/lib/ansible/module_utils/network/frr/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/frr/frr_bgp.py b/lib/ansible/modules/network/frr/frr_bgp.py new file mode 100644 index 00000000000..c2399026e1a --- /dev/null +++ b/lib/ansible/modules/network/frr/frr_bgp.py @@ -0,0 +1,414 @@ +#!/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: frr_bgp +version_added: "2.8" +author: "Nilashish Chakraborty (@nilashishc)" +short_description: Configure global BGP settings on Free Range Routing(FRR). +description: + - This module provides configuration management of global BGP parameters + on devices running Free Range Routing(FRR). +notes: + - Tested against FRRouting 6.0. +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. + 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 FRR sends keepalive messages to its peer. + - The range is from 0 to 65535. + type: int + required: True + holdtime: + description: + - Interval (in seconds) after not receiving a keepalive message that FRR declares a peer dead. + - The range is from 0 to 65535. + type: int + required: True + advertisement_interval: + description: + - Minimum interval between sending BGP routing updates for this neighbor. + type: int + local_as: + description: + - The local AS number for the neighbor. + type: int + port: + description: + - The TCP Port number to use for this neighbor. + - The range is from 0 to 65535. + type: int + 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 + 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','ospf6','eigrp','isis','table','static','connected','sharp','nhrp','kernel','babel','rip'] + required: True + id: + description: + - Specifies the instance ID/table ID for this protocol + - Valid for ospf and table + 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. + neighbors: + description: + - Specifies BGP neighbor related configurations in Address Family configuration mode. + suboptions: + neighbor: + description: + - Neighbor router address. + required: True + route_reflector_client: + description: + - Specify a neighbor as a route reflector client. + type: bool + route_server_client: + description: + - Specify a neighbor as a route server client. + type: bool + activate: + description: + - Enable the address family for this neighbor. + type: bool + remove_private_as: + description: + - Remove the private AS number from outbound updates. + type: bool + next_hop_self: + description: + - Enable/disable the next hop calculation for this neighbor. + type: bool + maximum_prefix: + description: + - Maximum number of prefixes to accept from this peer. + - The range is from 1 to 4294967295. + 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 + frr_bgp: + config: + bgp_as: 64496 + router_id: 192.0.2.1 + log_neighbor_changes: True + neighbors: + - neighbor: 192.51.100.1 + remote_as: 64497 + timers: + keepalive: 120 + holdtime: 360 + - neighbor: 198.51.100.2 + remote_as: 64498 + networks: + - prefix: 192.0.2.0 + masklen: 24 + route_map: RMAP_1 + - prefix: 198.51.100.0 + masklen: 24 + address_family: + - afi: ipv4 + safi: unicast + redistribute: + - protocol: ospf + id: 223 + metric: 10 + operation: merge + +- name: Configure BGP neighbors + frr_bgp: + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + password: ansible + description: IBGP_NBR_1 + timers: + keepalive: 120 + holdtime: 360 + - neighbor: 192.0.2.15 + remote_as: 64496 + description: IBGP_NBR_2 + advertisement_interval: 120 + operation: merge + +- name: Configure BGP neighbors under address family mode + frr_bgp: + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: multicast + neighbors: + - neighbor: 203.0.113.10 + activate: yes + maximum_prefix: 250 + + - neighbor: 192.0.2.15 + activate: yes + route_reflector_client: True + operation: merge + +- name: Configure root-level networks for BGP + frr_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: remove bgp as 64496 from config + frr_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 + - neighbor 192.51.100.1 remote-as 64497 + - neighbor 192.51.100.1 timers 120 360 + - neighbor 198.51.100.2 remote-as 64498 + - address-family ipv4 unicast + - redistribute ospf 223 metric 10 + - exit-address-family + - bgp log-neighbor-changes + - network 192.0.2.0/24 route-map RMAP_1 + - network 198.51.100.0/24 + - exit +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.network.frr.providers.module import NetworkModule +from ansible.module_utils.network.frr.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', required=True), + 'holdtime': dict(type='int', required=True) + } + + neighbor_spec = { + 'neighbor': dict(required=True), + 'remote_as': dict(type='int', required=True), + 'advertisement_interval': dict(type='int'), + 'local_as': dict(type='int'), + 'port': dict(type='int'), + '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(), + } + + af_neighbor_spec = { + 'neighbor': dict(required=True), + 'activate': dict(type='bool'), + 'remove_private_as': dict(type='bool'), + 'next_hop_self': dict(type='bool'), + 'route_reflector_client': dict(type='bool'), + 'route_server_client': dict(type='bool'), + 'maximum_prefix': dict(type='int') + } + + 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), + '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), + '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=' bgp') + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/frr/fixtures/frr_bgp_config b/test/units/modules/network/frr/fixtures/frr_bgp_config new file mode 100644 index 00000000000..eeecbe7ae60 --- /dev/null +++ b/test/units/modules/network/frr/fixtures/frr_bgp_config @@ -0,0 +1,24 @@ +! +router bgp 64496 + bgp router-id 192.0.2.1 + bgp log-neighbor-changes + neighbor 192.51.100.1 remote-as 64496 + neighbor 192.51.100.1 timers 120 360 + neighbor 198.51.100.3 remote-as 64498 + neighbor 2.2.2.2 remote-as 500 + neighbor 2.2.2.2 description EBGP_PEER + ! + address-family ipv4 unicast + network 192.0.1.0/24 route-map RMAP_1 + network 198.51.100.0/24 route-map RMAP_2 + redistribute static metric 100 + redistribute eigrp metric 10 route-map RMAP_3 + neighbor 2.2.2.2 remove-private-AS + neighbor 2.2.2.2 maximum-prefix 100 + exit-address-family + ! + address-family ipv4 multicast + network 10.0.0.0/8 route-map RMAP_1 + network 20.0.0.0/8 route-map RMAP_2 + exit-address-family +! diff --git a/test/units/modules/network/frr/frr_module.py b/test/units/modules/network/frr/frr_module.py index 58eba95dc13..b14d1fdb260 100644 --- a/test/units/modules/network/frr/frr_module.py +++ b/test/units/modules/network/frr/frr_module.py @@ -1,7 +1,7 @@ # -*- 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 diff --git a/test/units/modules/network/frr/test_frr_bgp.py b/test/units/modules/network/frr/test_frr_bgp.py new file mode 100644 index 00000000000..878c8fc99a1 --- /dev/null +++ b/test/units/modules/network/frr/test_frr_bgp.py @@ -0,0 +1,199 @@ +# +# (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.frr.providers.cli.config.bgp.process import Provider +from ansible.modules.network.frr import frr_bgp +from .frr_module import TestFrrModule, load_fixture + + +class TestFrrBgpModule(TestFrrModule): + module = frr_bgp + + def setUp(self): + super(TestFrrBgpModule, self).setUp() + self._bgp_config = load_fixture('frr_bgp_config') + + def test_frr_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', 'bgp router-id 192.0.2.2', 'exit']) + + def test_frr_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_frr_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_frr_bgp_neighbor(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, neighbors=[dict(neighbor='192.51.100.2', remote_as=64496)], + networks=None, address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, ['router bgp 64496', 'neighbor 192.51.100.2 remote-as 64496', 'exit']) + + def test_frr_bgp_neighbor_idempotent(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, neighbors=[dict(neighbor='192.51.100.1', remote_as=64496, + timers=dict(keepalive=120, holdtime=360))], + networks=None, address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_frr_bgp_network(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='192.0.2.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 192.0.2.0/24 route-map RMAP_1', 'exit'])) + + def test_frr_bgp_network_idempotent(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='192.0.1.0', masklen=24, 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_frr_bgp_address_family_redistribute(self): + rd_1 = dict(protocol='ospf', id='233', metric=90, route_map=None) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='unicast', redistribute=[rd_1])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + cmd = ['router bgp 64496', 'address-family ipv4 unicast', 'redistribute ospf 233 metric 90', + 'exit-address-family', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_frr_bgp_address_family_redistribute_idempotent(self): + rd_1 = dict(protocol='eigrp', metric=10, route_map='RMAP_3', id=None) + rd_2 = dict(protocol='static', metric=100, id=None, route_map=None) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='unicast', redistribute=[rd_1, rd_2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_frr_bgp_address_family_neighbors(self): + af_nbr_1 = dict(neighbor='192.51.100.1', maximum_prefix=35, activate=True) + af_nbr_2 = dict(neighbor='192.51.100.3', route_reflector_client=True, activate=True) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='multicast', 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 multicast', 'neighbor 192.51.100.1 activate', + 'neighbor 192.51.100.1 maximum-prefix 35', 'neighbor 192.51.100.3 activate', + 'neighbor 192.51.100.3 route-reflector-client', 'exit-address-family', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_frr_bgp_address_family_neighbors_idempotent(self): + af_nbr_1 = dict(neighbor='2.2.2.2', remove_private_as=True, maximum_prefix=100) + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='unicast', neighbors=[af_nbr_1])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_frr_bgp_address_family_networks(self): + net = dict(prefix='1.0.0.0', masklen=8, route_map='RMAP_1') + net2 = dict(prefix='192.168.1.0', masklen=24, route_map='RMAP_2') + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='multicast', 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 multicast', 'network 1.0.0.0/8 route-map RMAP_1', + 'network 192.168.1.0/24 route-map RMAP_2', 'exit-address-family', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_frr_bgp_address_family_networks_idempotent(self): + net = dict(prefix='10.0.0.0', masklen=8, route_map='RMAP_1') + net2 = dict(prefix='20.0.0.0', masklen=8, route_map='RMAP_2') + + config = dict(bgp_as=64496, address_family=[dict(afi='ipv4', safi='multicast', networks=[net, net2])], + networks=None) + + obj = Provider(params=dict(config=config, operation='merge')) + + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_frr_bgp_operation_override(self): + net_1 = dict(prefix='1.0.0.0', masklen=8, route_map='RMAP_1') + net_2 = dict(prefix='192.168.1.0', masklen=24, route_map='RMAP_2') + nbr_1 = dict(neighbor='192.51.100.1', remote_as=64496, advertisement_interval=120) + nbr_2 = dict(neighbor='192.51.100.3', remote_as=64496, timers=dict(keepalive=300, holdtime=360)) + af_nbr_1 = dict(neighbor='192.51.100.1', maximum_prefix=35) + af_nbr_2 = dict(neighbor='192.51.100.3', route_reflector_client=True) + + af_1 = dict(afi='ipv4', safi='unicast', neighbors=[af_nbr_1, af_nbr_2]) + af_2 = dict(afi='ipv4', safi='multicast', 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 192.51.100.1 remote-as 64496', + 'neighbor 192.51.100.1 advertisement-interval 120', 'neighbor 192.51.100.3 remote-as 64496', + 'neighbor 192.51.100.3 timers 300 360', 'address-family ipv4 unicast', + 'neighbor 192.51.100.1 maximum-prefix 35', 'neighbor 192.51.100.3 route-reflector-client', 'exit-address-family', + 'address-family ipv4 multicast', 'network 1.0.0.0/8 route-map RMAP_1', 'network 192.168.1.0/24 route-map RMAP_2', + 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_frr_bgp_operation_replace(self): + rd = dict(protocol='ospf', id=223, metric=110, route_map=None) + net = dict(prefix='10.0.0.0', masklen=8, route_map='RMAP_1') + net2 = dict(prefix='20.0.0.0', masklen=8, route_map='RMAP_2') + + af_1 = dict(afi='ipv4', safi='unicast', redistribute=[rd]) + af_2 = dict(afi='ipv4', safi='multicast', networks=[net, net2]) + + 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 unicast', 'redistribute ospf 223 metric 110', 'no redistribute eigrp', + 'no redistribute static', 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_frr_bgp_operation_replace_with_new_as(self): + rd = dict(protocol='ospf', id=223, metric=110, route_map=None) + + af_1 = dict(afi='ipv4', safi='unicast', redistribute=[rd]) + + config = dict(bgp_as=64497, address_family=[af_1], networks=None) + obj = Provider(params=dict(config=config, operation='replace')) + commands = obj.render(self._bgp_config) + + cmd = ['no router bgp 64496', 'router bgp 64497', 'address-family ipv4 unicast', 'redistribute ospf 223 metric 110', + 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd))