Tests for bgpcfgd templates (#4841)

* Tests for bgpcfgd templates
This commit is contained in:
pavel-shirshov 2020-06-25 14:54:02 -07:00 committed by GitHub
parent 719c8e68c8
commit d592e9b0f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1169 additions and 259 deletions

View File

@ -34,5 +34,5 @@
neighbor {{ bgp_session['name'] }} activate
exit-address-family
!
! end of template: bgpd/templates/BGP_SPEAKER/instance.conf.j2
! end of template: bgpd/templates/dynamic/instance.conf.j2
!

View File

@ -6,7 +6,7 @@
{# set the bgp neighbor timers if they have not default values #}
{% if (bgp_session['keepalive'] is defined and bgp_session['keepalive'] | int != 60)
or (bgp_session['holdtime'] is defined and bgp_session['holdtime'] | int != 180) %}
neighbor {{ neighbor_addr }} timers {{ bgp_session['keepalive'] }} {{ bgp_session['holdtime'] }}
neighbor {{ neighbor_addr }} timers {{ bgp_session['keepalive'] | default("60") }} {{ bgp_session['holdtime'] | default("180") }}
{% endif %}
!
{% if bgp_session.has_key('admin_status') and bgp_session['admin_status'] == 'down' or not bgp_session.has_key('admin_status') and CONFIG_DB__DEVICE_METADATA['localhost'].has_key('default_bgp_status') and CONFIG_DB__DEVICE_METADATA['localhost']['default_bgp_status'] == 'down' %}
@ -15,36 +15,43 @@
!
{% if neighbor_addr | ipv4 %}
address-family ipv4
{% if 'ASIC' in bgp_session['name'] %}
{% if 'ASIC' in bgp_session['name'] %}
neighbor {{ neighbor_addr }} peer-group PEER_V4_INT
{% else %}
{% else %}
neighbor {{ neighbor_addr }} peer-group PEER_V4
{% endif %}
!
{% if CONFIG_DB__DEVICE_METADATA['localhost']['sub_role'] == 'BackEnd' %}
neighbor {{ neighbor_addr }} route-map FROM_BGP_PEER_V4_INT in
{% endif %}
!
{% elif neighbor_addr | ipv6 %}
address-family ipv6
{% if 'ASIC' in bgp_session['name'] %}
{% if 'ASIC' in bgp_session['name'] %}
neighbor {{ neighbor_addr }} peer-group PEER_V6_INT
{% else %}
{% else %}
neighbor {{ neighbor_addr }} peer-group PEER_V6
{% endif %}
!
{% if CONFIG_DB__DEVICE_METADATA['localhost']['sub_role'] == 'BackEnd' %}
neighbor {{ neighbor_addr }} route-map FROM_BGP_PEER_V6_INT in
{% endif %}
{% endif %}
!
{% if bgp_session['rrclient'] | int != 0 %}
{% if bgp_session.has_key('rrclient') and bgp_session['rrclient'] | int != 0 %}
neighbor {{ neighbor_addr }} route-reflector-client
{% endif %}
{% endif %}
!
{% if bgp_session['nhopself'] | int != 0 %}
{% if bgp_session.has_key('nhopself') and bgp_session['nhopself'] | int != 0 %}
neighbor {{ neighbor_addr }} next-hop-self
{% endif %}
{% if 'ASIC' in bgp_session['name'] %}
{% endif %}
!
{% if 'ASIC' in bgp_session['name'] %}
neighbor {{ neighbor_addr }} next-hop-self force
{% endif %}
{% endif %}
!
neighbor {{ neighbor_addr }} activate
exit-address-family
!
{% if bgp_session["asn"] == bgp_asn and CONFIG_DB__DEVICE_METADATA['localhost']['type'] == "SpineChassisFrontendRouter" %}
address-family l2vpn evpn
@ -52,8 +59,6 @@
advertise-all-vni
exit-address-family
{% endif %}
neighbor {{ neighbor_addr }} activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -2,5 +2,7 @@
build/
dist/
*.egg-info/
app/*.pyc
tests/*.pyc
tests/__pycache__/
.idea

View File

View File

@ -0,0 +1,103 @@
import os
import tempfile
from .vars import g_debug
from .log import log_crit, log_err
from .util import run_command
class ConfigMgr(object):
""" The class represents frr configuration """
def __init__(self):
self.current_config = None
def reset(self):
""" Reset stored config """
self.current_config = None
def update(self):
""" Read current config from FRR """
self.current_config = None
ret_code, out, err = run_command(["vtysh", "-c", "show running-config"])
if ret_code != 0:
log_crit("can't update running config: rc=%d out='%s' err='%s'" % (ret_code, out, err))
return
self.current_config = self.to_canonical(out)
def push(self, cmd):
"""
Push new changes to FRR
:param cmd: configuration change for FRR. Type: String
:return: True if change was applied successfully, False otherwise
"""
return self.write(cmd)
def write(self, cmd):
"""
Write configuration change to FRR.
:param cmd: new configuration to write into FRR. Type: String
:return: True if change was applied successfully, False otherwise
"""
fd, tmp_filename = tempfile.mkstemp(dir='/tmp')
os.close(fd)
with open(tmp_filename, 'w') as fp:
fp.write("%s\n" % cmd)
command = ["vtysh", "-f", tmp_filename]
ret_code, out, err = run_command(command)
if not g_debug:
os.remove(tmp_filename)
if ret_code != 0:
err_tuple = str(cmd), ret_code, out, err
log_err("ConfigMgr::push(): can't push configuration '%s', rc='%d', stdout='%s', stderr='%s'" % err_tuple)
if ret_code == 0:
self.current_config = None # invalidate config
return ret_code == 0
@staticmethod
def to_canonical(raw_config):
"""
Convert FRR config into canonical format
:param raw_config: config in frr format
:return: frr config in canonical format
"""
parsed_config = []
lines_with_comments = raw_config.split("\n")
lines = [line for line in lines_with_comments
if not line.strip().startswith('!') and line.strip() != '']
if len(lines) == 0:
return []
cur_path = [lines[0]]
cur_offset = ConfigMgr.count_spaces(lines[0])
for line in lines:
n_spaces = ConfigMgr.count_spaces(line)
s_line = line.strip()
# assert(n_spaces == cur_offset or (n_spaces + 1) == cur_offset or (n_spaces - 1) == cur_offset)
if n_spaces == cur_offset:
cur_path[-1] = s_line
elif n_spaces > cur_offset:
cur_path.append(s_line)
elif n_spaces < cur_offset:
cur_path = cur_path[:-2]
cur_path.append(s_line)
parsed_config.append(cur_path[:])
cur_offset = n_spaces
return parsed_config
@staticmethod
def count_spaces(line):
""" Count leading spaces in the line """
return len(line) - len(line.lstrip())
@staticmethod
def from_canonical(canonical_config):
"""
Convert config from canonical format into FRR raw format
:param canonical_config: config in a canonical format
:return: config in the FRR raw format
"""
out = ""
for lines in canonical_config:
spaces = len(lines) - 1
out += " " * spaces + lines[-1] + "\n"
return out

View File

@ -0,0 +1,33 @@
import syslog
from .vars import g_debug
def log_debug(msg):
""" Send a message msg to the syslog as DEBUG """
if g_debug:
syslog.syslog(syslog.LOG_DEBUG, msg)
def log_notice(msg):
""" Send a message msg to the syslog as NOTICE """
syslog.syslog(syslog.LOG_NOTICE, msg)
def log_info(msg):
""" Send a message msg to the syslog as INFO """
syslog.syslog(syslog.LOG_INFO, msg)
def log_warn(msg):
""" Send a message msg to the syslog as WARNING """
syslog.syslog(syslog.LOG_WARNING, msg)
def log_err(msg):
""" Send a message msg to the syslog as ERR """
syslog.syslog(syslog.LOG_ERR, msg)
def log_crit(msg):
""" Send a message msg to the syslog as CRIT """
syslog.syslog(syslog.LOG_CRIT, msg)

View File

@ -0,0 +1,98 @@
from collections import OrderedDict
from functools import partial
import jinja2
import netaddr
class TemplateFabric(object):
""" Fabric for rendering jinja2 templates """
def __init__(self, template_path = '/usr/share/sonic/templates'):
j2_template_paths = [template_path]
j2_loader = jinja2.FileSystemLoader(j2_template_paths)
j2_env = jinja2.Environment(loader=j2_loader, trim_blocks=False)
j2_env.filters['ipv4'] = self.is_ipv4
j2_env.filters['ipv6'] = self.is_ipv6
j2_env.filters['pfx_filter'] = self.pfx_filter
for attr in ['ip', 'network', 'prefixlen', 'netmask']:
j2_env.filters[attr] = partial(self.prefix_attr, attr)
self.env = j2_env
def from_file(self, filename):
"""
Read a template from a file
:param filename: filename of the file. Type String
:return: Jinja2 template object
"""
return self.env.get_template(filename)
def from_string(self, tmpl):
"""
Read a template from a string
:param tmpl: Text representation of Jinja2 template
:return: Jinja2 template object
"""
return self.env.from_string(tmpl)
@staticmethod
def is_ipv4(value):
""" Return True if the value is an ipv4 address """
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return False
return addr.version == 4
@staticmethod
def is_ipv6(value):
""" Return True if the value is an ipv6 address """
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return False
return addr.version == 6
@staticmethod
def prefix_attr(attr, value):
"""
Extract attribute from IPNetwork object
:param attr: attribute to extract
:param value: the string representation of ip prefix which will be converted to IPNetwork.
:return: the value of the extracted attribute
"""
if not value:
return None
else:
try:
prefix = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return None
return str(getattr(prefix, attr))
@staticmethod
def pfx_filter(value):
"""INTERFACE Table can have keys in one of the two formats:
string or tuple - This filter skips the string keys and only
take into account the tuple.
For eg - VLAN_INTERFACE|Vlan1000 vs VLAN_INTERFACE|Vlan1000|192.168.0.1/21
"""
table = OrderedDict()
if not value:
return table
for key, val in value.items():
if not isinstance(key, tuple):
continue
table[key] = val
return table

View File

@ -0,0 +1,22 @@
import subprocess
from .log import log_debug, log_err
def run_command(command, shell=False, hide_errors=False):
"""
Run a linux command. The command is defined as a list. See subprocess.Popen documentation on format
:param command: command to execute. Type: List of strings
:param shell: execute the command through shell when True. Type: Boolean
:param hide_errors: don't report errors to syslog when True. Type: Boolean
:return: Tuple: integer exit code from the command, stdout as a string, stderr as a string
"""
log_debug("execute command '%s'." % str(command))
p = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
if not hide_errors:
print_tuple = p.returncode, str(command), stdout, stderr
log_err("command execution returned %d. Command: '%s', stdout: '%s', stderr: '%s'" % print_tuple)
return p.returncode, stdout, stderr

View File

@ -0,0 +1 @@
g_debug = False

View File

@ -1,260 +1,27 @@
#!/usr/bin/env python
import sys
import subprocess
import datetime
import time
import syslog
import signal
import traceback
import os
import tempfile
import json
from collections import defaultdict, OrderedDict
from pprint import pprint
from functools import partial
from collections import defaultdict
import yaml
import jinja2
import netaddr
from swsscommon import swsscommon
from app.vars import g_debug
from app.log import log_debug, log_notice, log_info, log_warn, log_err, log_crit
from app.template import TemplateFabric
from app.config import ConfigMgr
from app.util import run_command
g_run = True
g_debug = False
def log_debug(msg):
""" Send a message msg to the syslog as DEBUG """
if g_debug:
syslog.syslog(syslog.LOG_DEBUG, msg)
def log_notice(msg):
""" Send a message msg to the syslog as NOTICE """
syslog.syslog(syslog.LOG_NOTICE, msg)
def log_info(msg):
""" Send a message msg to the syslog as INFO """
syslog.syslog(syslog.LOG_INFO, msg)
def log_warn(msg):
""" Send a message msg to the syslog as WARNING """
syslog.syslog(syslog.LOG_WARNING, msg)
def log_err(msg):
""" Send a message msg to the syslog as ERR """
syslog.syslog(syslog.LOG_ERR, msg)
def log_crit(msg):
""" Send a message msg to the syslog as CRIT """
syslog.syslog(syslog.LOG_CRIT, msg)
def run_command(command, shell=False, hide_errors=False):
"""
Run a linux command. The command is defined as a list. See subprocess.Popen documentation on format
:param command: command to execute. Type: List of strings
:param shell: execute the command through shell when True. Type: Boolean
:param hide_errors: don't report errors to syslog when True. Type: Boolean
:return: Tuple: integer exit code from the command, stdout as a string, stderr as a string
"""
log_debug("execute command '%s'." % str(command))
p = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
if not hide_errors:
print_tuple = p.returncode, str(command), stdout, stderr
log_err("command execution returned %d. Command: '%s', stdout: '%s', stderr: '%s'" % print_tuple)
return p.returncode, stdout, stderr
class ConfigMgr(object):
""" The class represents frr configuration """
def __init__(self):
self.current_config = None
def reset(self):
""" Reset stored config """
self.current_config = None
def update(self):
""" Read current config from FRR """
self.current_config = None
ret_code, out, err = run_command(["vtysh", "-c", "show running-config"])
if ret_code != 0:
log_crit("can't update running config: rc=%d out='%s' err='%s'" % (ret_code, out, err))
return
self.current_config = self.to_canonical(out)
def push(self, cmd):
"""
Push new changes to FRR
:param cmd: configuration change for FRR. Type: String
:return: True if change was applied successfully, False otherwise
"""
return self.write(cmd)
def write(self, cmd):
"""
Write configuration change to FRR.
:param cmd: new configuration to write into FRR. Type: String
:return: True if change was applied successfully, False otherwise
"""
fd, tmp_filename = tempfile.mkstemp(dir='/tmp')
os.close(fd)
with open(tmp_filename, 'w') as fp:
fp.write("%s\n" % cmd)
command = ["vtysh", "-f", tmp_filename]
ret_code, out, err = run_command(command)
if not g_debug:
os.remove(tmp_filename)
if ret_code != 0:
err_tuple = str(cmd), ret_code, out, err
log_err("ConfigMgr::push(): can't push configuration '%s', rc='%d', stdout='%s', stderr='%s'" % err_tuple)
if ret_code == 0:
self.current_config = None # invalidate config
return ret_code == 0
@staticmethod
def to_canonical(raw_config):
"""
Convert FRR config into canonical format
:param raw_config: config in frr format
:return: frr config in canonical format
"""
parsed_config = []
cur_offset = 0
lines = raw_config.split("\n")
cur_path = [lines[0]]
for line in lines:
if line.strip().startswith('!') or line.strip() == '':
continue
n_spaces = ConfigMgr.count_spaces(line)
s_line = line.strip()
assert(n_spaces == cur_offset or (n_spaces + 1) == cur_offset or (n_spaces - 1) == cur_offset)
if n_spaces == cur_offset:
cur_path[-1] = s_line
elif n_spaces > cur_offset:
cur_path.append(s_line)
elif n_spaces < cur_offset:
cur_path = cur_path[:-2]
cur_path.append(s_line)
parsed_config.append(cur_path[:])
cur_offset = n_spaces
return parsed_config
@staticmethod
def count_spaces(line):
""" Count leading spaces in the line """
return len(line) - len(line.lstrip())
@staticmethod
def from_canonical(canonical_config):
"""
Convert config from canonical format into FRR raw format
:param canonical_config: config in a canonical format
:return: config in the FRR raw format
"""
out = ""
for lines in canonical_config:
spaces = len(lines) - 1
out += " " * spaces + lines[-1] + "\n"
return out
class TemplateFabric(object):
""" Fabric for rendering jinja2 templates """
def __init__(self):
j2_template_paths = ['/usr/share/sonic/templates']
j2_loader = jinja2.FileSystemLoader(j2_template_paths)
j2_env = jinja2.Environment(loader=j2_loader, trim_blocks=False)
j2_env.filters['ipv4'] = self.is_ipv4
j2_env.filters['ipv6'] = self.is_ipv6
j2_env.filters['pfx_filter'] = self.pfx_filter
for attr in ['ip', 'network', 'prefixlen', 'netmask']:
j2_env.filters[attr] = partial(self.prefix_attr, attr)
self.env = j2_env
def from_file(self, filename):
"""
Read a template from a file
:param filename: filename of the file. Type String
:return: Jinja2 template object
"""
return self.env.get_template(filename)
def from_string(self, tmpl):
"""
Read a template from a string
:param tmpl: Text representation of Jinja2 template
:return: Jinja2 template object
"""
return self.env.from_string(tmpl)
@staticmethod
def is_ipv4(value):
""" Return True if the value is an ipv4 address """
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return False
return addr.version == 4
@staticmethod
def is_ipv6(value):
""" Return True if the value is an ipv6 address """
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return False
return addr.version == 6
@staticmethod
def prefix_attr(attr, value):
"""
Extract attribute from IPNetwork object
:param attr: attribute to extract
:param value: the string representation of ip prefix which will be converted to IPNetwork.
:return: the value of the extracted attribute
"""
if not value:
return None
else:
try:
prefix = netaddr.IPNetwork(str(value))
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
return None
return str(getattr(prefix, attr))
@staticmethod
def pfx_filter(value):
"""INTERFACE Table can have keys in one of the two formats:
string or tuple - This filter skips the string keys and only
take into account the tuple.
For eg - VLAN_INTERFACE|Vlan1000 vs VLAN_INTERFACE|Vlan1000|192.168.0.1/21
"""
table = OrderedDict()
if not value:
return table
for key, val in value.items():
if not isinstance(key, tuple):
continue
table[key] = val
return table
class Directory(object):

View File

@ -1,13 +1,14 @@
#!/usr/bin/env python
from setuptools import setup
import setuptools
setup(name='sonic-bgpcfgd',
setuptools.setup(name='sonic-bgpcfgd',
version='1.0',
description='Utility to dynamically generate BGP configuration for FRR',
author='Pavel Shirshov',
author_email='pavelsh@microsoft.com',
url='https://github.com/Azure/sonic-buildimage',
packages=setuptools.find_packages(),
scripts=['bgpcfgd'],
install_requires=['jinja2>=2.10', 'netaddr', 'pyyaml'],
setup_requires=['pytest-runner', 'pytest'],

View File

@ -0,0 +1,21 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"deployment_id": "5"
}
},
"CONFIG_DB__LOOPBACK_INTERFACE": {
"Loopback1|55.55.55.55/32": {}
},
"bgp_session": {
"ip_range": "10.10.20.0/24,20.20.20.0/24",
"name": "dyn_name",
"peer_asn": "11111",
"src_address": "1.1.1.1"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,19 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"deployment_id": "5"
}
},
"CONFIG_DB__LOOPBACK_INTERFACE": {
"Loopback1|55.55.55.55/32": {}
},
"bgp_session": {
"ip_range": "10.10.20.0/24,20.20.20.0/24",
"name": "dyn_name"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,22 @@
!
! template: bgpd/templates/dynamic/instance.conf.j2
!
neighbor dyn_name peer-group
neighbor dyn_name passive
neighbor dyn_name ebgp-multihop 255
neighbor dyn_name soft-reconfiguration inbound
neighbor dyn_name route-map FROM_BGP_SPEAKER in
neighbor dyn_name route-map TO_BGP_SPEAKER out
neighbor dyn_name remote-as 11111
bgp listen range 10.10.20.0/24 peer-group dyn_name
bgp listen range 20.20.20.0/24 peer-group dyn_name
neighbor dyn_name update-source 1.1.1.1
address-family ipv4
neighbor dyn_name activate
exit-address-family
address-family ipv6
neighbor dyn_name activate
exit-address-family
!
! end of template: bgpd/templates/dynamic/instance.conf.j2
!

View File

@ -0,0 +1,22 @@
!
! template: bgpd/templates/dynamic/instance.conf.j2
!
neighbor dyn_name peer-group
neighbor dyn_name passive
neighbor dyn_name ebgp-multihop 255
neighbor dyn_name soft-reconfiguration inbound
neighbor dyn_name route-map FROM_BGP_SPEAKER in
neighbor dyn_name route-map TO_BGP_SPEAKER out
neighbor dyn_name remote-as 51111
bgp listen range 10.10.20.0/24 peer-group dyn_name
bgp listen range 20.20.20.0/24 peer-group dyn_name
neighbor dyn_name update-source 55.55.55.55
address-family ipv4
neighbor dyn_name activate
exit-address-family
address-family ipv6
neighbor dyn_name activate
exit-address-family
!
! end of template: bgpd/templates/dynamic/instance.conf.j2
!

View File

@ -0,0 +1,7 @@
!
! template: bgpd/templates/BGP_SPEAKER/peer-group.conf.j2
!
! nothing is here
!
! end of template: bgpd/templates/BGP_SPEAKER/peer-group.conf.j2
!

View File

@ -0,0 +1,9 @@
!
! template: bgpd/templates/BGP_SPEAKER/policies.conf.j2
!
route-map FROM_BGP_SPEAKER permit 10
!
route-map TO_BGP_SPEAKER deny 1
!
! end of template: bgpd/templates/BGP_SPEAKER/policies.conf.j2
!

View File

@ -0,0 +1,15 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "_ASIC_"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,15 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "FC00::",
"bgp_session": {
"asn": "555",
"name": "_ASIC_"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,23 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"sub_role": "BackEnd"
}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer",
"keepalive": "5",
"holdtime": "30",
"admin_status": "down",
"ASIC": "something",
"rrclient": "1",
"nhopself": "1"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,23 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"sub_role": "BackEnd"
}
},
"neighbor_addr": "fc::10",
"bgp_session": {
"asn": "555",
"name": "remote_peer",
"keepalive": "5",
"holdtime": "30",
"admin_status": "down",
"ASIC": "something",
"rrclient": "1",
"nhopself": "1"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,15 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,15 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "fc00::2",
"bgp_session": {
"asn": "555",
"name": "remote_peer"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,18 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"type": "SpineChassisFrontendRouter"
}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
},
"bgp_asn": "555"
}

View File

@ -0,0 +1,17 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"default_bgp_status": "down"
}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,17 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"default_bgp_status": "up"
}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,16 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer",
"keepalive": "5"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,16 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {}
},
"neighbor_addr": "10.10.10.10",
"bgp_session": {
"asn": "555",
"name": "remote_peer",
"holdtime": "240"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description _ASIC_
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4_INT
neighbor 10.10.10.10 next-hop-self force
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor FC00:: remote-as 555
neighbor FC00:: description _ASIC_
address-family ipv6
neighbor FC00:: peer-group PEER_V6_INT
neighbor FC00:: next-hop-self force
neighbor FC00:: activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,17 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
neighbor 10.10.10.10 timers 5 30
neighbor 10.10.10.10 shutdown
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 route-map FROM_BGP_PEER_V4_INT in
neighbor 10.10.10.10 route-reflector-client
neighbor 10.10.10.10 next-hop-self
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,17 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor fc::10 remote-as 555
neighbor fc::10 description remote_peer
neighbor fc::10 timers 5 30
neighbor fc::10 shutdown
address-family ipv6
neighbor fc::10 peer-group PEER_V6
neighbor fc::10 route-map FROM_BGP_PEER_V6_INT in
neighbor fc::10 route-reflector-client
neighbor fc::10 next-hop-self
neighbor fc::10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,12 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,12 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor fc00::2 remote-as 555
neighbor fc00::2 description remote_peer
address-family ipv6
neighbor fc00::2 peer-group PEER_V6
neighbor fc00::2 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,16 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
address-family l2vpn evpn
neighbor 10.10.10.10 activate
advertise-all-vni
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
neighbor 10.10.10.10 shutdown
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,12 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
neighbor 10.10.10.10 timers 5 180
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/general/instance.conf.j2
!
neighbor 10.10.10.10 remote-as 555
neighbor 10.10.10.10 description remote_peer
neighbor 10.10.10.10 timers 60 240
address-family ipv4
neighbor 10.10.10.10 peer-group PEER_V4
neighbor 10.10.10.10 activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!

View File

@ -0,0 +1,7 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"type": "ToRRouter"
}
}
}

View File

@ -0,0 +1,8 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"type": "LeafRouter",
"sub_role": "BackEnd"
}
}
}

View File

@ -0,0 +1,30 @@
!
! template: bgpd/templates/general/peer-group.conf.j2
!
neighbor PEER_V4 peer-group
neighbor PEER_V4_INT peer-group
neighbor PEER_V6 peer-group
neighbor PEER_V6_INT peer-group
address-family ipv4
neighbor PEER_V4 allowas-in 1
neighbor PEER_V4_INT allowas-in 1
neighbor PEER_V4 soft-reconfiguration inbound
neighbor PEER_V4 route-map FROM_BGP_PEER_V4 in
neighbor PEER_V4 route-map TO_BGP_PEER_V4 out
neighbor PEER_V4_INT soft-reconfiguration inbound
neighbor PEER_V4_INT route-map FROM_BGP_PEER_V4 in
neighbor PEER_V4_INT route-map TO_BGP_PEER_V4 out
exit-address-family
address-family ipv6
neighbor PEER_V6 allowas-in 1
neighbor PEER_V6_INT allowas-in 1
neighbor PEER_V6 soft-reconfiguration inbound
neighbor PEER_V6 route-map FROM_BGP_PEER_V6 in
neighbor PEER_V6 route-map TO_BGP_PEER_V6 out
neighbor PEER_V6_INT soft-reconfiguration inbound
neighbor PEER_V6_INT route-map FROM_BGP_PEER_V6 in
neighbor PEER_V6_INT route-map TO_BGP_PEER_V6 out
exit-address-family
!
! end of template: bgpd/templates/general/peer-group.conf.j2
!

View File

@ -0,0 +1,28 @@
!
! template: bgpd/templates/general/peer-group.conf.j2
!
neighbor PEER_V4 peer-group
neighbor PEER_V4_INT peer-group
neighbor PEER_V6 peer-group
neighbor PEER_V6_INT peer-group
address-family ipv4
neighbor PEER_V4_INT route-reflector-client
neighbor PEER_V4 soft-reconfiguration inbound
neighbor PEER_V4 route-map FROM_BGP_PEER_V4 in
neighbor PEER_V4 route-map TO_BGP_PEER_V4 out
neighbor PEER_V4_INT soft-reconfiguration inbound
neighbor PEER_V4_INT route-map FROM_BGP_PEER_V4 in
neighbor PEER_V4_INT route-map TO_BGP_PEER_V4 out
exit-address-family
address-family ipv6
neighbor PEER_V6_INT route-reflector-client
neighbor PEER_V6 soft-reconfiguration inbound
neighbor PEER_V6 route-map FROM_BGP_PEER_V6 in
neighbor PEER_V6 route-map TO_BGP_PEER_V6 out
neighbor PEER_V6_INT soft-reconfiguration inbound
neighbor PEER_V6_INT route-map FROM_BGP_PEER_V6 in
neighbor PEER_V6_INT route-map TO_BGP_PEER_V6 out
exit-address-family
!
! end of template: bgpd/templates/general/peer-group.conf.j2
!

View File

@ -0,0 +1,8 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"sub_role": "BackEnd"
}
},
"loopback0_ipv4": "10.10.10.10/32"
}

View File

@ -0,0 +1,8 @@
{
"CONFIG_DB__DEVICE_METADATA": {
"localhost": {
"sub_role": "NotBackEnd"
}
},
"loopback0_ipv4": "10.10.10.10/32"
}

View File

@ -0,0 +1,25 @@
!
! template: bgpd/templates/general/policies.conf.j2
!
route-map FROM_BGP_PEER_V4 permit 100
!
route-map TO_BGP_PEER_V4 permit 100
!
route-map FROM_BGP_PEER_V6 permit 1
set ipv6 next-hop prefer-global
!
route-map FROM_BGP_PEER_V6 permit 100
!
route-map TO_BGP_PEER_V6 permit 100
!
route-map FROM_BGP_PEER_V4_INT permit 2
set originator-id 10.10.10.10
!
route-map FROM_BGP_PEER_V6_INT permit 1
set ipv6 next-hop prefer-global
!
route-map FROM_BGP_PEER_V6_INT permit 2
set originator-id 10.10.10.10
!
! end of template: bgpd/templates/general/policies.conf.j2
!

View File

@ -0,0 +1,16 @@
!
! template: bgpd/templates/general/policies.conf.j2
!
route-map FROM_BGP_PEER_V4 permit 100
!
route-map TO_BGP_PEER_V4 permit 100
!
route-map FROM_BGP_PEER_V6 permit 1
set ipv6 next-hop prefer-global
!
route-map FROM_BGP_PEER_V6 permit 100
!
route-map TO_BGP_PEER_V6 permit 100
!
! end of template: bgpd/templates/general/policies.conf.j2
!

View File

@ -0,0 +1,12 @@
{
"bgp_asn": "555",
"neighbor_addr": "10.20.30.40",
"bgp_session": {
"name": "monitor"
},
"constants": {
"deployment_id_asn_map": {
"5": "51111"
}
}
}

View File

@ -0,0 +1,13 @@
!
! template: bgpd/templates/monitors/instance.conf.j2
!
neighbor 10.20.30.40 remote-as 555
neighbor 10.20.30.40 peer-group BGPMON
neighbor 10.20.30.40 description monitor
neighbor 10.20.30.40 activate
address-family ipv6
neighbor 10.20.30.40 activate
exit-address-family
!
! end of template: bgpd/templates/BGPMON/instance.conf.j2
!

View File

@ -0,0 +1,3 @@
{
"loopback0_ipv4": "1.1.1.1/32"
}

View File

@ -0,0 +1,12 @@
!
! template: bgpd/templates/BGPMON/peer-group.conf.j2
!
neighbor BGPMON peer-group
neighbor BGPMON update-source 1.1.1.1
neighbor BGPMON route-map FROM_BGPMON in
neighbor BGPMON route-map TO_BGPMON out
neighbor BGPMON send-community
neighbor BGPMON maximum-prefix 1
!
! end of template: bgpd/templates/BGPMON/peer-group.conf.j2
!

View File

@ -0,0 +1,9 @@
!
! template: bgpd/templates/BGPMON/policies.conf.j2
!
route-map FROM_BGPMON deny 10
!
route-map TO_BGPMON permit 10
!
! end of template: bgpd/templates/BGPMON/policies.conf.j2
!

View File

@ -0,0 +1,114 @@
import os
import re
from app.template import TemplateFabric
from .util import load_constants
TEMPLATE_PATH = os.path.abspath('../../dockers/docker-fpm-frr/frr')
def parse_instance_conf(filename):
activate_re = re.compile(r'^neighbor\s+(\S+)\s+activate$')
with open(filename) as fp:
lines = [line.strip() for line in fp if not line.strip().startswith('!') and line.strip() != '']
# Search all v6 neighbors
neighbors = {}
for line in lines:
if activate_re.match(line):
neighbor = activate_re.match(line).group(1)
if TemplateFabric.is_ipv6(neighbor):
neighbors[neighbor] = {}
# Extract peer-groups and route-maps
for neighbor, neighbor_data in neighbors.iteritems():
route_map_in_re = re.compile(r'^neighbor\s+%s\s+route-map\s+(\S+) in$' % neighbor)
peer_group_re = re.compile(r'^neighbor\s+%s\s+peer-group\s+(\S+)$' % neighbor)
for line in lines:
if route_map_in_re.match(line):
assert "route-map" not in neighbor_data
neighbor_data["route-map"] = route_map_in_re.match(line).group(1)
if peer_group_re.match(line):
assert "peer-group" not in neighbor_data
neighbor_data["peer-group"] = peer_group_re.match(line).group(1)
# Ensure that every ivp6 neighbor has either route-map or peer-group
for neighbor, neighbor_data in neighbors.iteritems():
assert "route-map" in neighbor_data or "peer-group" in neighbor_data,\
"IPv6 neighbor '%s' must have either route-map in or peer-group %s" % (neighbor, neighbor_data)
return neighbors
def load_results(path, dir_name):
result_files = []
for fname in os.listdir(os.path.join(path, dir_name)):
if not fname.startswith("result_"):
continue
full_fname = os.path.join(path, dir_name, fname)
if not os.path.isfile(full_fname):
continue
result_files.append(full_fname)
return result_files
def process_instances(path):
result_files = load_results(path, "instance.conf")
# search for ipv6 neighbors
neighbors_list = []
for fname in result_files:
neighbors = parse_instance_conf(fname)
if neighbors:
neighbors_list.append(neighbors)
return neighbors_list
def parse_peer_group_conf(filename, pg_name):
route_map_re = re.compile(r'^neighbor\s+%s\s+route-map\s+(\S+)\s+in$' % pg_name)
with open(filename) as fp:
lines = [line.strip() for line in fp if not line.strip().startswith('!') and line.strip() != '']
route_maps = set()
for line in lines:
if route_map_re.match(line):
route_map = route_map_re.match(line).group(1)
route_maps.add(route_map)
return route_maps
def extract_rm_from_peer_group(path, peer_group_name):
result_files = load_results(path, "peer-group.conf")
rm_set = set()
for fname in result_files:
route_maps = parse_peer_group_conf(fname, peer_group_name)
if route_maps:
rm_set |= route_maps
return list(rm_set)
def check_routemap_in_file(filename, route_map_name):
route_map_re = re.compile(r'^route-map\s+%s\s+(\S+)' % route_map_name)
set_re = re.compile(r'set ipv6 next-hop prefer-global')
with open(filename) as fp:
lines = [line.strip() for line in fp if not line.strip().startswith('!') and line.strip() != '']
found_first_entry = False
for line in lines:
err_msg = "route-map %s doesn't have mandatory 'set ipv6 next-hop prefer-global' entry as the first rule" % route_map_name
assert not (found_first_entry and line.startswith("route-map")), err_msg
if found_first_entry and set_re.match(line):
break # We're good
if route_map_re.match(line):
err_msg = "route-map %s doesn't have mandatory permit entry for 'set ipv6 next-hop prefer-global" % route_map_name
assert route_map_re.match(line).group(1) == 'permit', err_msg
found_first_entry = True
return found_first_entry
def check_routemap(path, route_map_name):
result_files = load_results(path, "policies.conf")
checked = False
for fname in result_files:
checked = checked or check_routemap_in_file(fname, route_map_name)
assert checked, "route-map %s wasn't found" % route_map_name
def test_v6_next_hop_global():
paths = ["tests/data/%s" % value for value in load_constants().values()]
for path in paths:
test_cases = process_instances(path)
for test_case in test_cases:
for neighbor_value in test_case.values():
if 'route-map' in neighbor_value:
check_routemap(path, neighbor_value['route-map'])
elif 'peer-group' in neighbor_value:
route_map_in_list = extract_rm_from_peer_group(path, neighbor_value['peer-group'])
for route_map_in in route_map_in_list:
check_routemap(path, route_map_in)

View File

@ -1,4 +0,0 @@
import pytest
def test_sample():
assert True

View File

@ -0,0 +1,129 @@
import os
import json
from app.template import TemplateFabric
from app.config import ConfigMgr
from .util import load_constants
TEMPLATE_PATH = os.path.abspath('../../dockers/docker-fpm-frr/frr')
def load_tests(peer_type, template_name):
constants = load_constants()
path = "tests/data/%s/%s" % (constants[peer_type], template_name)
param_files = [name for name in os.listdir(path)
if os.path.isfile(os.path.join(path, name)) and name.startswith("param_")]
tests = []
for param_fname in param_files:
casename = param_fname.replace("param_", "").replace(".json", "")
result_fname = "result_%s.conf" % casename
full_param_fname = os.path.join(path, param_fname)
full_result_fname = os.path.join(path, result_fname)
tests.append((casename, full_param_fname, full_result_fname))
tmpl_path = os.path.join("bgpd", "templates", constants[peer_type], "%s.j2" % template_name)
return tmpl_path, tests
def load_json(fname):
with open(fname) as param_fp:
raw_params = json.load(param_fp)
params = {}
for table_key, table_entries in raw_params.items():
if table_key.startswith("CONFIG_DB__"):
# convert CONFIG_DB__* entries keys into tuple if needed
new_table_entries = {}
for entry_key, entry_value in table_entries.items():
if '|' in entry_key:
new_key = tuple(entry_key.split('|'))
else:
new_key = entry_key
new_table_entries[new_key] = entry_value
params[table_key] = new_table_entries
else:
params[table_key] = table_entries
return params
def compress_comments(raw_config):
comment_counter = 0
output = []
for line in raw_config.split('\n'):
stripped_line = line.strip()
# Skip empty lines
if stripped_line == '':
pass
# Write lines without comments
elif not stripped_line.startswith('!'):
if comment_counter > 0:
output.append("!")
comment_counter = 0
output.append(line)
# Write non-empty comments
elif stripped_line.startswith('!') and len(stripped_line) > 1:
if comment_counter > 0:
output.append("!")
comment_counter = 0
output.append(line)
# Count empty comments
else: # stripped_line == '!'
comment_counter += 1
# Flush last comment if we have one
if comment_counter > 0:
output.append("!")
return "\n".join(output) + "\n"
def write_result(fname, raw_result):
with open(fname, 'w') as fp:
raw_result_w_commpressed_comments = compress_comments(raw_result)
fp.write(raw_result_w_commpressed_comments)
def run_tests(test_name, template_fname, tests):
tf = TemplateFabric(TEMPLATE_PATH)
template = tf.from_file(template_fname)
for case_name, param_fname, result_fname in tests:
params = load_json(param_fname)
raw_generated_result = str(template.render(params))
assert "None" not in raw_generated_result, "Test %s.%s" % (test_name, case_name)
# this is used only for initial generation write_result(result_fname, raw_generated_result)
canonical_generated_result = ConfigMgr.to_canonical(raw_generated_result)
with open(result_fname) as result_fp:
raw_saved_result = result_fp.read()
canonical_saved_result = ConfigMgr.to_canonical(raw_saved_result)
assert canonical_saved_result == canonical_generated_result, "Test %s.%s" % (test_name, case_name)
# Tests
def test_general_policies():
test_data = load_tests("general", "policies.conf")
run_tests("general_policies", *test_data)
def test_general_pg():
test_data = load_tests("general", "peer-group.conf")
run_tests("general_pg", *test_data)
def test_general_instance():
test_data = load_tests("general", "instance.conf")
run_tests("general_instance", *test_data)
def test_dynamic_policies():
test_data = load_tests("dynamic", "policies.conf")
run_tests("dynamic_policies", *test_data)
def test_dynamic_pg():
test_data = load_tests("dynamic", "peer-group.conf")
run_tests("dynamic_pg", *test_data)
def test_dynamic_instance():
test_data = load_tests("dynamic", "instance.conf")
run_tests("dynamic_instance", *test_data)
def test_monitors_policies():
test_data = load_tests("monitors", "policies.conf")
run_tests("monitors_policies", *test_data)
def test_monitors_pg():
test_data = load_tests("monitors", "peer-group.conf")
run_tests("monitors_pg", *test_data)
def test_monitors_instance():
test_data = load_tests("monitors", "instance.conf")
run_tests("monitors_instance", *test_data)

View File

@ -0,0 +1,16 @@
import os
import yaml
CONSTANTS_PATH = os.path.abspath('../../files/image_config/constants/constants.yml')
def load_constants():
with open(CONSTANTS_PATH) as f:
data = yaml.load(f)
result = {}
assert "constants" in data, "'constants' key not found in constants.yml"
assert "bgp" in data["constants"], "'bgp' key not found in constants.yml"
assert "peers" in data["constants"]["bgp"], "'peers' key not found in constants.yml"
for name, value in data["constants"]["bgp"]["peers"].items():
assert "template_dir" in value, "'template_dir' key not found for peer '%s'" % name
result[name] = value["template_dir"]
return result