From 55bfa18c0c0b68628c4a1f0bd3fd4c9e9a32a3e3 Mon Sep 17 00:00:00 2001 From: Nilashish Chakraborty Date: Mon, 4 Mar 2019 13:37:57 +0530 Subject: [PATCH] New module to support BGP configuration management in IOS (#49121) * ios_bgp initial push Signed-off-by: NilashishC * Added tests for ios_bgp Signed-off-by: NilashishC * Fixed docs Signed-off-by: NilashishC * Added space Signed-off-by: NilashishC * Fix Shippable Errors Signed-off-by: NilashishC * Fix Shippable Errors Signed-off-by: NilashishC * Add support for af_neighbor option Signed-off-by: NilashishC * Add support for af_neighbor option - 2 Signed-off-by: NilashishC * Add support for af_neighbor option - 3 Signed-off-by: NilashishC * Fix typo Signed-off-by: NilashishC * Refactor BGP Signed-off-by: NilashishC * Fix CI and previous reviews Signed-off-by: NilashishC * Add missing params documentation Signed-off-by: NilashishC * Remove previous tests Signed-off-by: NilashishC * Remove elements=dict from keys with type=list from args spec for element validation to pass Signed-off-by: NilashishC * Added function to validate input Signed-off-by: NilashishC * Fix sanity test failure Signed-off-by: NilashishC * Minor bug fixes Signed-off-by: NilashishC * Fix typo in fixture Signed-off-by: NilashishC * Add integration tests Signed-off-by: NilashishC --- .../network/ios/providers/__init__.py | 0 .../network/ios/providers/cli/__init__.py | 0 .../ios/providers/cli/config/__init__.py | 0 .../network/ios/providers/cli/config/base.py | 77 ++++ .../ios/providers/cli/config/bgp/__init__.py | 0 .../cli/config/bgp/address_family.py | 140 ++++++ .../ios/providers/cli/config/bgp/neighbors.py | 186 ++++++++ .../ios/providers/cli/config/bgp/process.py | 139 ++++++ .../network/ios/providers/module.py | 62 +++ .../network/ios/providers/providers.py | 120 +++++ lib/ansible/modules/network/ios/ios_bgp.py | 430 ++++++++++++++++++ .../targets/ios_bgp/defaults/main.yaml | 2 + .../targets/ios_bgp/meta/main.yaml | 2 + .../targets/ios_bgp/tasks/cli.yaml | 16 + .../targets/ios_bgp/tasks/main.yaml | 2 + .../targets/ios_bgp/tests/cli/basic.yaml | 394 ++++++++++++++++ .../network/ios/fixtures/ios_bgp_config.cfg | 24 + .../units/modules/network/ios/test_ios_bgp.py | 207 +++++++++ 18 files changed, 1801 insertions(+) create mode 100644 lib/ansible/module_utils/network/ios/providers/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/base.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/bgp/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/bgp/address_family.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/bgp/neighbors.py create mode 100644 lib/ansible/module_utils/network/ios/providers/cli/config/bgp/process.py create mode 100644 lib/ansible/module_utils/network/ios/providers/module.py create mode 100644 lib/ansible/module_utils/network/ios/providers/providers.py create mode 100644 lib/ansible/modules/network/ios/ios_bgp.py create mode 100644 test/integration/targets/ios_bgp/defaults/main.yaml create mode 100644 test/integration/targets/ios_bgp/meta/main.yaml create mode 100644 test/integration/targets/ios_bgp/tasks/cli.yaml create mode 100644 test/integration/targets/ios_bgp/tasks/main.yaml create mode 100644 test/integration/targets/ios_bgp/tests/cli/basic.yaml create mode 100644 test/units/modules/network/ios/fixtures/ios_bgp_config.cfg create mode 100644 test/units/modules/network/ios/test_ios_bgp.py diff --git a/lib/ansible/module_utils/network/ios/providers/__init__.py b/lib/ansible/module_utils/network/ios/providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/providers/cli/__init__.py b/lib/ansible/module_utils/network/ios/providers/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/providers/cli/config/__init__.py b/lib/ansible/module_utils/network/ios/providers/cli/config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/providers/cli/config/base.py b/lib/ansible/module_utils/network/ios/providers/cli/config/base.py new file mode 100644 index 00000000000..eade249a09c --- /dev/null +++ b/lib/ansible/module_utils/network/ios/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/ios/providers/cli/config/bgp/__init__.py b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/address_family.py b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/address_family.py new file mode 100644 index 00000000000..366f6413c1b --- /dev/null +++ b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/address_family.py @@ -0,0 +1,140 @@ +# +# (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.ios.providers.providers import CliProvider +from ansible.module_utils.network.ios.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'] + if item['safi'] != 'unicast': + context += ' %s' % 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'] + cmd = 'network %s' % network + if entry['masklen']: + cmd += ' mask %s' % to_netmask(entry['masklen']) + network += ' mask %s' % to_netmask(entry['masklen']) + if entry['route_map']: + cmd += ' route-map %s' % entry['route_map'] + network += ' route-map %s' % entry['route_map'] + + safe_list.append(network) + + if not config or cmd not in config: + commands.append(cmd) + + if self.params['operation'] == 'replace': + if config: + matches = re.findall(r'network (.*)', 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', 'ospfv3', 'eigrp'): + 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/ios/providers/cli/config/bgp/neighbors.py b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/neighbors.py new file mode 100644 index 00000000000..d1ac03ff281 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/neighbors.py @@ -0,0 +1,186 @@ +# +# (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.ios.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_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: + 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_timers(self, item, config): + """generate bgp timer related configuration + """ + keepalive = item['timers']['keepalive'] + holdtime = item['timers']['holdtime'] + min_neighbor_holdtime = item['timers']['min_neighbor_holdtime'] + neighbor = item['neighbor'] + + if keepalive and holdtime: + cmd = 'neighbor %s timers %s %s' % (neighbor, keepalive, holdtime) + if min_neighbor_holdtime: + cmd += ' %s' % min_neighbor_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_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_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/ios/providers/cli/config/bgp/process.py b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/process.py new file mode 100644 index 00000000000..5ace7fcfbb3 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/providers/cli/config/bgp/process.py @@ -0,0 +1,139 @@ +# +# (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.ios.providers.providers import register_provider +from ansible.module_utils.network.ios.providers.providers import CliProvider +from ansible.module_utils.network.ios.providers.cli.config.bgp.neighbors import Neighbors +from ansible.module_utils.network.ios.providers.cli.config.bgp.address_family import AddressFamily +from ansible.module_utils.common.network import to_netmask + +REDISTRIBUTE_PROTOCOLS = frozenset(['ospf', 'ospfv3', 'eigrp', 'isis', 'static', 'connected', + 'odr', 'lisp', 'mobile', 'rip']) + + +@register_provider('ios', 'ios_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 = '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'] + cmd = 'network %s' % network + if entry['masklen']: + cmd += ' mask %s' % to_netmask(entry['masklen']) + network += ' mask %s' % to_netmask(entry['masklen']) + + if entry['route_map']: + cmd += ' route-map %s' % entry['route_map'] + network += ' route-map %s' % entry['route_map'] + + safe_list.append(network) + + if not config or cmd not in config: + commands.append(cmd) + + if self.params['operation'] == 'replace': + if config: + matches = re.findall(r'network (.*)', 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=None): + 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': + if address_family and root_networks: + for item in address_family: + if item['networks']: + raise ValueError('operation is replace but provided both root level network(s) and network(s) under %s %s address family' + % (item['afi'], item['safi'])) + + if root_networks and 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/ios/providers/module.py b/lib/ansible/module_utils/network/ios/providers/module.py new file mode 100644 index 00000000000..e7dc037e5b2 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/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.ios.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/ios/providers/providers.py b/lib/ansible/module_utils/network/ios/providers/providers.py new file mode 100644 index 00000000000..a466b033d94 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/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/ios/ios_bgp.py b/lib/ansible/modules/network/ios/ios_bgp.py new file mode 100644 index 00000000000..aa94f1ac6dd --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_bgp.py @@ -0,0 +1,430 @@ +#!/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: ios_bgp +version_added: "2.8" +author: "Nilashish Chakraborty (@nilashishc)" +short_description: Configure global BGP protocol settings on Cisco IOS. +description: + - This module provides configuration management of global BGP parameters + on devices running Cisco IOS +notes: + - Tested against Cisco IOS Version 15.6(3)M2 +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 device 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 IOS declares a peer dead. + - The range is from 0 to 65535. + type: int + required: True + min_neighbor_holdtime: + description: + - Interval (in seconds) specifying the minimum acceptable hold-time from a BGP neighbor. + - The minimum acceptable hold-time must be less than, or equal to, the interval specified in the holdtime argument. + - The range is from 0 to 65535. + type: int + local_as: + description: + - The local AS number for the neighbor. + 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 + synchronization: + description: + - Enable/disable IGP synchronization. + type: bool + auto_summary: + description: + - Enable/disable automatic network number summarization. + type: bool + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + choices: ['ospf', 'ospfv3', 'eigrp', 'isis', 'static', 'connected', 'odr', 'lisp', 'mobile', 'rip'] + required: True + id: + description: + - Identifier for the routing protocol for configuring redistribute information. + - Valid for protocols 'ospf', 'ospfv3' and 'eigrp'. + 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: + 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 + advertisement_interval: + description: + - Minimum interval between sending BGP routing updates for this neighbor. + type: int + 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 + next_hop_unchanged: + description: + - Propagate next hop unchanged for iBGP paths to this neighbor. + type: bool + maximum_prefix: + description: + - Maximum number of prefixes to accept from this peer. + - The range is from 1 to 2147483647. + 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 + ios_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 + min_neighbor_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: ospf + id: 223 + metric: 10 + operation: merge + +- name: Configure BGP neighbors + ios_bgp: + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + password: ansible + description: IBGP_NBR_1 + ebgp_multihop: 100 + timers: + keepalive: 300 + holdtime: 360 + min_neighbor_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 + ios_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 + ios_bgp: + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: unicast + neighbors: + - neighbor: 203.0.113.10 + activate: yes + maximum_prefix: 250 + advertisement_interval: 120 + - neighbor: 192.0.2.15 + activate: yes + route_reflector_client: True + operation: merge + +- name: remove bgp as 64496 from config + ios_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 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 ospf 223 metric 70 + - exit-address-family +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.network.ios.providers.module import NetworkModule +from ansible.module_utils.network.ios.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), + 'id': dict(), + 'metric': dict(type='int'), + 'route_map': dict(), + } + + timer_spec = { + 'keepalive': dict(type='int', required=True), + 'holdtime': dict(type='int', required=True), + 'min_neighbor_holdtime': dict(type='int'), + } + + neighbor_spec = { + 'neighbor': dict(required=True), + 'remote_as': dict(type='int', required=True), + 'local_as': 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'), + 'advertisement_interval': dict(type='int'), + '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'), + 'auto_summary': dict(type='bool'), + 'synchronization': dict(type='bool'), + '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='| 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/ios_bgp/defaults/main.yaml b/test/integration/targets/ios_bgp/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/ios_bgp/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/ios_bgp/meta/main.yaml b/test/integration/targets/ios_bgp/meta/main.yaml new file mode 100644 index 00000000000..159cea8d383 --- /dev/null +++ b/test/integration/targets/ios_bgp/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_bgp/tasks/cli.yaml b/test/integration/targets/ios_bgp/tasks/cli.yaml new file mode 100644 index 00000000000..ea5c8c3742f --- /dev/null +++ b/test/integration/targets/ios_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 }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_bgp/tasks/main.yaml b/test/integration/targets/ios_bgp/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_bgp/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_bgp/tests/cli/basic.yaml b/test/integration/targets/ios_bgp/tests/cli/basic.yaml new file mode 100644 index 00000000000..97803c12576 --- /dev/null +++ b/test/integration/targets/ios_bgp/tests/cli/basic.yaml @@ -0,0 +1,394 @@ +- debug: msg="START ios cli/ios_bgp.yaml on connection={{ ansible_connection }}" + +- name: Clear existing BGP config + ios_bgp: + operation: delete + ignore_errors: yes + +- name: Configure BGP with AS 64496 and a router-id + ios_bgp: &config + operation: merge + config: + bgp_as: 64496 + router_id: 192.0.2.2 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'bgp router-id 192.0.2.2' in result.commands" + +- name: Configure BGP with AS 64496 and a router-id (idempotent) + ios_bgp: *config + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors + ios_bgp: &nbr + operation: merge + config: + bgp_as: 64496 + neighbors: + - neighbor: 192.0.2.10 + remote_as: 64496 + password: ansible + description: IBGP_NBR_1 + ebgp_multihop: 100 + timers: + keepalive: 300 + holdtime: 360 + min_neighbor_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 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) + ios_bgp: *nbr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors with operation replace + ios_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 + local_as: 64497 + 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" + - "'neighbor 203.0.113.10 local-as 64497' in result.commands" + - "'no neighbor 192.0.2.10' in result.commands" + +- name: Configure BGP neighbors with operation replace (idempotent) + ios_bgp: *nbr_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure root-level networks for BGP + ios_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 mask 255.255.255.224 route-map RMAP_1' in result.commands" + - "'network 203.0.113.32 mask 255.255.255.224 route-map RMAP_2' in result.commands" + +- name: Configure root-level networks for BGP (idempotent) + ios_bgp: *net + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure root-level networks for BGP with operation replace + ios_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 mask 255.255.255.240' in result.commands" + - "'no network 203.0.113.32 mask 255.255.255.224 route-map RMAP_2' in result.commands" + +- name: Configure root-level networks for BGP with operation replace (idempotent) + ios_bgp: *net_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure BGP neighbors under address family mode + ios_bgp: &af_nbr + operation: merge + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: unicast + neighbors: + - neighbor: 203.0.113.10 + activate: yes + maximum_prefix: 250 + advertisement_interval: 120 + + - neighbor: 192.0.2.15 + activate: yes + route_reflector_client: 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 maximum-prefix 250' in result.commands" + - "'neighbor 203.0.113.10 advertisement-interval 120' in result.commands" + - "'neighbor 192.0.2.15 activate' in result.commands" + - "'neighbor 192.0.2.15 route-reflector-client' in result.commands" + +- name: Configure BGP neighbors under address family mode (idempotent) + ios_bgp: *af_nbr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family + ios_bgp: &af_net + operation: merge + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: multicast + 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: ipv4 + safi: unicast + networks: + - prefix: 198.51.100.64 + masklen: 28 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4 multicast' in result.commands" + - "'network 198.51.100.48 mask 255.255.255.240 route-map RMAP_1' in result.commands" + - "'network 192.0.2.64 mask 255.255.255.224' in result.commands" + - "'network 203.0.113.160 mask 255.255.255.224 route-map RMAP_2' in result.commands" + - "'exit-address-family' in result.commands" + - "'address-family ipv4' in result.commands" + - "'network 198.51.100.64 mask 255.255.255.240' in result.commands" + - "'exit-address-family' in result.commands" + +- name: Configure networks under address family (idempotent) + ios_bgp: *af_net + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure networks under address family with operation replace + ios_bgp: &af_net_rplc + operation: replace + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: multicast + networks: + - prefix: 198.51.100.80 + masklen: 28 + + - prefix: 192.0.2.64 + masklen: 27 + + - prefix: 203.0.113.192 + masklen: 27 + + - afi: ipv4 + safi: unicast + networks: + - prefix: 198.51.100.64 + masklen: 28 + register: result + +- assert: + that: + - 'result.changed == true' + - '"router bgp 64496" in result.commands' + - '"address-family ipv4 multicast" in result.commands' + - '"network 198.51.100.80 mask 255.255.255.240" in result.commands' + - '"network 203.0.113.192 mask 255.255.255.224" in result.commands' + - '"no network 198.51.100.48 mask 255.255.255.240 route-map RMAP_1" in result.commands' + - '"no network 203.0.113.160 mask 255.255.255.224 route-map RMAP_2" in result.commands' + - '"exit-address-family" in result.commands' + +- name: Configure networks under address family with operation replace (idempotent) + ios_bgp: *af_net_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure redistribute information under address family mode + ios_bgp: &af_rdr + operation: merge + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: multicast + redistribute: + - protocol: ospf + id: 112 + metric: 64 + + - protocol: eigrp + id: 233 + metric: 256 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4 multicast' in result.commands" + - "'redistribute ospf 112 metric 64' in result.commands" + - "'redistribute eigrp 233 metric 256' in result.commands" + - "'exit-address-family' in result.commands" + +- name: Configure redistribute information under address family mode (idempotent) + ios_bgp: *af_rdr + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure redistribute information under address family mode with operation replace + ios_bgp: &af_rdr_rplc + operation: replace + config: + bgp_as: 64496 + address_family: + - afi: ipv4 + safi: multicast + redistribute: + - protocol: ospf + id: 112 + metric: 64 + register: result + +- assert: + that: + - 'result.changed == true' + - "'router bgp 64496' in result.commands" + - "'address-family ipv4 multicast' in result.commands" + - "'no redistribute eigrp 233' in result.commands" + - "'exit-address-family' in result.commands" + +- name: Configure redistribute information under address family mode with operation replace (idempotent) + ios_bgp: *af_rdr_rplc + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Override all the exisiting BGP config + ios_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" + - "'bgp router-id 192.0.2.10' in result.commands" + - "'bgp log-neighbor-changes' in result.commands" + +- name: Teardown + ios_bgp: &rm + operation: delete + register: result + +- assert: + that: + - 'result.changed == true' + - "'no router bgp 64497' in result.commands" + +- name: Teardown again (idempotent) + ios_bgp: *rm + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END ios cli/ios_bgp.yaml on connection={{ ansible_connection }}" diff --git a/test/units/modules/network/ios/fixtures/ios_bgp_config.cfg b/test/units/modules/network/ios/fixtures/ios_bgp_config.cfg new file mode 100644 index 00000000000..9fec934aa4a --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_bgp_config.cfg @@ -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 360 + neighbor 198.51.100.3 remote-as 64498 + neighbor 203.0.113.5 remote-as 500 + neighbor 203.0.113.5 description EBGP_PEER + ! + address-family ipv4 + network 192.0.2.0 mask 255.255.254.0 route-map RMAP_1 + network 198.51.100.0 mask 255.255.255.128 route-map RMAP_2 + redistribute static metric 100 + redistribute eigrp metric 10 route-map RMAP_3 + neighbor 203.0.113.1 remove-private-as + neighbor 203.0.113.1 maximum-prefix 100 + exit-address-family + ! + address-family ipv4 multicast + network 203.0.113.0 mask 255.255.255.224 route-map RMAP_1 + network 192.0.2.0 mask 255.255.255.192 route-map RMAP_2 + exit-address-family +! diff --git a/test/units/modules/network/ios/test_ios_bgp.py b/test/units/modules/network/ios/test_ios_bgp.py new file mode 100644 index 00000000000..792219778ca --- /dev/null +++ b/test/units/modules/network/ios/test_ios_bgp.py @@ -0,0 +1,207 @@ +# +# (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.ios.providers.cli.config.bgp.process import Provider +from ansible.modules.network.ios import ios_bgp +from .ios_module import TestIosModule, load_fixture + + +class TestIosBgpModule(TestIosModule): + module = ios_bgp + + def setUp(self): + super(TestIosBgpModule, self).setUp() + self._bgp_config = load_fixture('ios_bgp_config.cfg') + + def test_ios_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_ios_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_ios_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_ios_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_ios_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, + min_neighbor_holdtime=360))], + networks=None, address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_ios_bgp_network(self): + obj = Provider(params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='192.0.1.0', masklen=23, 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.1.0 mask 255.255.254.0 route-map RMAP_1', + 'exit'])) + + def test_ios_bgp_network_idempotent(self): + obj = Provider( + params=dict(config=dict(bgp_as=64496, networks=[dict(prefix='192.0.2.0', masklen=23, route_map='RMAP_1'), + dict(prefix='198.51.100.0', masklen=25, + route_map='RMAP_2')], + address_family=None), + operation='merge')) + commands = obj.render(self._bgp_config) + self.assertEqual(commands, []) + + def test_ios_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', 'redistribute ospf 233 metric 90', + 'exit-address-family', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_ios_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_ios_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_ios_bgp_address_family_neighbors_idempotent(self): + af_nbr_1 = dict(neighbor='203.0.113.1', 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_ios_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 mask 255.0.0.0 route-map RMAP_1', + 'network 192.168.1.0 mask 255.255.255.0 route-map RMAP_2', 'exit-address-family', 'exit'] + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_ios_bgp_address_family_networks_idempotent(self): + net = dict(prefix='203.0.113.0', masklen=27, route_map='RMAP_1') + net2 = dict(prefix='192.0.2.0', masklen=26, 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_ios_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, update_source='GigabitEthernet0/1') + nbr_2 = dict(neighbor='192.51.100.3', remote_as=64496, timers=dict(keepalive=300, holdtime=360, + min_neighbor_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 update-source GigabitEthernet0/1', 'neighbor 192.51.100.3 remote-as 64496', + 'neighbor 192.51.100.3 timers 300 360 360', 'address-family ipv4', + '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 mask 255.0.0.0 route-map RMAP_1', + 'network 192.168.1.0 mask 255.255.255.0 route-map RMAP_2', + 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_ios_bgp_operation_replace(self): + rd = dict(protocol='ospf', id=223, metric=110, route_map=None) + net = dict(prefix='203.0.113.0', masklen=27, route_map='RMAP_1') + net2 = dict(prefix='192.0.2.0', masklen=26, 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', 'redistribute ospf 223 metric 110', + 'no redistribute eigrp', + 'no redistribute static', 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd)) + + def test_ios_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', + 'redistribute ospf 223 metric 110', + 'exit-address-family', 'exit'] + + self.assertEqual(sorted(commands), sorted(cmd))