Skip to content

Commit

Permalink
Tests for bgpcfgd templates (#4841)
Browse files Browse the repository at this point in the history
* Tests for bgpcfgd templates
  • Loading branch information
pavel-shirshov authored Jun 25, 2020
1 parent 719c8e6 commit d592e9b
Show file tree
Hide file tree
Showing 59 changed files with 1,169 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
!
31 changes: 18 additions & 13 deletions dockers/docker-fpm-frr/frr/bgpd/templates/general/instance.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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' %}
Expand All @@ -15,45 +15,50 @@
!
{% 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
neighbor {{ neighbor_addr }} activate
advertise-all-vni
exit-address-family
{% endif %}
neighbor {{ neighbor_addr }} activate
exit-address-family
!
! end of template: bgpd/templates/general/instance.conf.j2
!
2 changes: 2 additions & 0 deletions src/sonic-bgpcfgd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
build/
dist/
*.egg-info/
app/*.pyc
tests/*.pyc
tests/__pycache__/
.idea
Empty file.
103 changes: 103 additions & 0 deletions src/sonic-bgpcfgd/app/config.py
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions src/sonic-bgpcfgd/app/log.py
Original file line number Diff line number Diff line change
@@ -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)
98 changes: 98 additions & 0 deletions src/sonic-bgpcfgd/app/template.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions src/sonic-bgpcfgd/app/util.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/sonic-bgpcfgd/app/vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
g_debug = False
Loading

0 comments on commit d592e9b

Please sign in to comment.