mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-23 05:10:22 -07:00
New module to support BGP configuration management in IOS (#49121)
* ios_bgp initial push Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Added tests for ios_bgp Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fixed docs Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Added space Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix Shippable Errors Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix Shippable Errors Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Add support for af_neighbor option Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Add support for af_neighbor option - 2 Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Add support for af_neighbor option - 3 Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix typo Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Refactor BGP Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix CI and previous reviews Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Add missing params documentation Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Remove previous tests Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Remove elements=dict from keys with type=list from args spec for element validation to pass Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Added function to validate input Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix sanity test failure Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Minor bug fixes Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Fix typo in fixture Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com> * Add integration tests Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
This commit is contained in:
parent
bf3fc86437
commit
55bfa18c0c
18 changed files with 1801 additions and 0 deletions
|
@ -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
|
|
@ -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'])
|
|
@ -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
|
|
@ -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')
|
Loading…
Add table
Add a link
Reference in a new issue