Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split default bgp config into main config and peer template #3627

Merged
merged 11 commits into from
Oct 24, 2019
221 changes: 158 additions & 63 deletions dockers/docker-fpm-frr/bgpcfgd
Original file line number Diff line number Diff line change
@@ -1,124 +1,219 @@
#!/usr/bin/env python

import sys
import copy
import Queue
import redis
import subprocess
import syslog
import signal
import traceback
import os
import shutil
import tempfile
import json
from collections import defaultdict
from cStringIO import StringIO
from pprint import pprint

import jinja2
import netaddr
from swsscommon import swsscommon


def run_command(command):
syslog.syslog(syslog.LOG_DEBUG, "execute command {}.".format(command))
p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
stdout = p.communicate()[0]
p.wait()
g_run = True
g_debug = False


def run_command(command, shell=False):
str_cmd = " ".join(command)
if g_debug:
syslog.syslog(syslog.LOG_DEBUG, "execute command {}.".format(str_cmd))
p = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
syslog.syslog(syslog.LOG_ERR, 'command execution returned {}. Command: "{}", stdout: "{}"'.format(p.returncode, command, stdout))
syslog.syslog(syslog.LOG_ERR, 'command execution returned {}. Command: "{}", stdout: "{}", stderr: "{}"'.format(p.returncode, str_cmd, stdout, stderr))

return p.returncode, stdout, stderr

class TemplateFabric(object):
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=True)
j2_env.filters['ipv4'] = self.is_ipv4
j2_env.filters['ipv6'] = self.is_ipv6
self.env = j2_env

def from_file(self, filename):
return self.env.get_template(filename)

def from_string(self, tmpl):
return self.env.from_string(tmpl)

@staticmethod
def is_ipv4(value):
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except:
return False
return addr.version == 4

@staticmethod
def is_ipv6(value):
if not value:
return False
if isinstance(value, netaddr.IPNetwork):
addr = value
else:
try:
addr = netaddr.IPNetwork(str(value))
except:
return False
return addr.version == 6


class BGPConfigManager(object):
def __init__(self, daemon):
self.daemon = daemon
self.bgp_asn = None
self.bgp_message = Queue.Queue(0)
self.meta = None
self.bgp_messages = []
self.peers = self.load_peers() # we can have bgp monitors peers here. it could be fixed by adding support for it here
fabric = TemplateFabric()
self.bgp_peer_add_template = fabric.from_file('bgpd.peer.conf.j2')
self.bgp_peer_del_template = fabric.from_string('no neighbor {{ neighbor_addr }}')
daemon.add_manager(swsscommon.CONFIG_DB, swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, self.__metadata_handler)
daemon.add_manager(swsscommon.CONFIG_DB, swsscommon.CFG_BGP_NEIGHBOR_TABLE_NAME, self.__bgp_handler)

def load_peers(self):
peers = set()
command = ["vtysh", "-c", "show bgp neighbors json"]
rc, out, err = run_command(command)
if rc == 0:
js_bgp = json.loads(out)
peers = set(js_bgp.keys())
return peers

def __metadata_handler(self, key, op, data):
if key != "localhost" \
or "bgp_asn" not in data \
or self.bgp_asn == data["bgp_asn"]:
return

# TODO add ASN update commands
# TODO add ASN update commands

self.meta = { 'localhost': data }
self.bgp_asn = data["bgp_asn"]
self.__update_bgp()

def __update_bgp(self):
while not self.bgp_message.empty():
key, op, data = self.bgp_message.get()
syslog.syslog(syslog.LOG_INFO, 'value for {} changed to {}'.format(key, data))
cmds = []
for key, op, data in self.bgp_messages:
add_peer_chunk = self.bgp_peer_add_template.render(DEVICE_METADATA=self.meta, neighbor_addr=key, bgp_session=data)
del_peer_chunk = self.bgp_peer_del_template.render(neighbor_addr=key)
if op == swsscommon.SET_COMMAND:
command = "vtysh -c 'configure terminal' -c 'router bgp {}' -c 'neighbor {} remote-as {}'".format(self.bgp_asn, key, data['asn'])
run_command(command)
if "name" in data:
command = "vtysh -c 'configure terminal' -c 'router bgp {}' -c 'neighbor {} description {}'".format(self.bgp_asn, key, data['name'])
run_command(command)
if "admin_status" in data:
command_mod = "no " if data["admin_status"] == "up" else ""
command = "vtysh -c 'configure terminal' -c 'router bgp {}' -c '{}neighbor {} shutdown'".format(self.bgp_asn, command_mod, key)
pavel-shirshov marked this conversation as resolved.
Show resolved Hide resolved
run_command(command)
if key in self.peers:
cmds.append(del_peer_chunk)
cmds.append(add_peer_chunk)
syslog.syslog(syslog.LOG_INFO, 'Peer {} added with attributes {}'.format(key, data))
self.peers.add(key)
elif op == swsscommon.DEL_COMMAND:
# Neighbor is deleted
command = "vtysh -c 'configure terminal' -c 'router bgp {}' -c 'no neighbor {}'".format(self.bgp_asn, key)
run_command(command)
if key in self.peers:
cmds.append(del_peer_chunk)
self.peers.remove(key)
syslog.syslog(syslog.LOG_INFO, 'Peer {} removed'.format(key))
else:
syslog.syslog(syslog.LOG_WARNING, 'Peer {} not found'.format(key))
self.bgp_messages = []

if len(cmds) == 0:
return

fd, tmp_filename = tempfile.mkstemp(dir='/tmp')
os.close(fd)
with open (tmp_filename, 'w') as fp:
fp.write('router bgp %s\n' % self.bgp_asn)
for cmd in cmds:
fp.write("%s\n" % cmd)

command = ["vtysh", "-f", tmp_filename]
run_command(command) #FIXME
os.remove(tmp_filename)

def __bgp_handler(self, key, op, data):
self.bgp_message.put((key, op, data))
self.bgp_messages.append((key, op, data))
# If ASN is not set, we just cache this message until the ASN is set.
if self.bgp_asn == None:
return
self.__update_bgp()
if self.bgp_asn is not None:
self.__update_bgp()


class Daemon(object):

SELECT_TIMEOUT = 1000
SUPPORT_DATABASE_LIST = (swsscommon.APPL_DB, swsscommon.CONFIG_DB)
DATABASE_LIST = [ swsscommon.CONFIG_DB ]

def __init__(self):
self.appl_db = swsscommon.DBConnector(swsscommon.APPL_DB, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0)
self.conf_db = swsscommon.DBConnector(swsscommon.CONFIG_DB, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0)
self.db_connectors = { db : swsscommon.DBConnector(db, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0) for db in Daemon.DATABASE_LIST }
self.selector = swsscommon.Select()
self.db_connectors = {}
self.callbacks = {}
self.callbacks = defaultdict(lambda : defaultdict(list)) # db -> table -> []
self.subscribers = set()

def get_db_connector(self, db):
if db not in Daemon.SUPPORT_DATABASE_LIST:
raise ValueError("database {} not Daemon support list {}.".format(db, SUPPORT_DATABASE_LIST))
# if this database connector has been initialized
if db not in self.db_connectors:
self.db_connectors[db] = swsscommon.DBConnector(db, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0)
return self.db_connectors[db]

def add_manager(self, db, table_name, callback):
if db not in self.callbacks:
self.callbacks[db] = {}
if db not in Daemon.DATABASE_LIST:
raise ValueError("database {} isn't supported. Supported '{}' only.".format(db, ",".join(Daemon.DATABASE_LIST)))

if table_name not in self.callbacks[db]:
self.callbacks[db][table_name] = []
conn = self.get_db_connector(db)
conn = self.db_connectors[db]
subscriber = swsscommon.SubscriberStateTable(conn, table_name)
self.subscribers.add(subscriber)
self.selector.addSelectable(subscriber)
self.callbacks[db][table_name].append(callback)

def start(self):
while True:
state, selectable = self.selector.select(Daemon.SELECT_TIMEOUT)
if not selectable:
def run(self):
while g_run:
state, _ = self.selector.select(Daemon.SELECT_TIMEOUT)
if state == self.selector.TIMEOUT:
continue
elif state == self.selector.ERROR:
raise Exception("Received error from select")

for subscriber in self.subscribers:
key, op, fvs = subscriber.pop()
# if no new message
if not key:
continue
data = dict(fvs)
syslog.syslog(syslog.LOG_DEBUG, "Receive message : {}".format((key, op, fvs)))
if g_debug:
syslog.syslog(syslog.LOG_DEBUG, "Received message : {}".format((key, op, fvs)))
for callback in self.callbacks[subscriber.getDbConnector().getDbId()][subscriber.getTableName()]:
callback(key, op, data)
callback(key, op, dict(fvs))


def signal_handler(signum, frame):
global g_run
g_run = False


def main():
syslog.openlog("bgpcfgd")
daemon = Daemon()
bgp_manager = BGPConfigManager(daemon)
daemon.start()
syslog.closelog()

if __name__ == "__main__":
main()
daemon.run()


if __name__ == '__main__':
rc = 0
try:
syslog.openlog('bgpcfgd')
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
main()
except KeyboardInterrupt:
syslog.syslog(syslog.LOG_NOTICE, "Keyboard interrupt")
except Exception as e:
syslog.syslog(syslog.LOG_CRIT, "Got an exception %s: Traceback: %s" % (str(e), traceback.format_exc()))
rc = -1
finally:
syslog.closelog()
try:
sys.exit(rc)
except SystemExit:
os._exit(rc)
71 changes: 19 additions & 52 deletions dockers/docker-fpm-frr/bgpd.conf.default.j2
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ route-map TO_BGP_SPEAKER_V4 deny 10
{% if prefix | ipv4 and name == 'Loopback0' %}
pavel-shirshov marked this conversation as resolved.
Show resolved Hide resolved
ip prefix-list PL_LoopbackV4 permit {{ prefix | ip }}/32
{% elif prefix | ipv6 and name == 'Loopback0' %}
ipv6 prefix-list PL_LoopbackV6 permit {{ prefix | ip }}/64
ipv6 prefix-list PL_LoopbackV6 permit {{ prefix | replace('/128', '/64') | ip_network }}/64
{% endif %}
{% endfor %}
!
Expand Down Expand Up @@ -65,63 +65,32 @@ router bgp {{ DEVICE_METADATA['localhost']['bgp_asn'] }}
{% endif %}
{% endfor %}
{% endblock vlan_advertisement %}
{% block bgp_sessions %}
{% for neighbor_addr, bgp_session in BGP_NEIGHBOR.iteritems() %}
{% if bgp_session['asn'] | int != 0 %}
neighbor {{ neighbor_addr }} remote-as {{ bgp_session['asn'] }}
neighbor {{ neighbor_addr }} description {{ bgp_session['name'] }}
{# 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'] }}
{% endif %}
{% if bgp_session.has_key('admin_status') and bgp_session['admin_status'] == 'down' or not bgp_session.has_key('admin_status') and DEVICE_METADATA['localhost'].has_key('default_bgp_status') and DEVICE_METADATA['localhost']['default_bgp_status'] == 'down' %}
neighbor {{ neighbor_addr }} shutdown
{% endif %}
{# Apply default route-map for v4 peers #}
{% if neighbor_addr | ipv4 %}
neighbor {{ neighbor_addr }} route-map TO_BGP_PEER_V4 out
{% endif %}
{% if neighbor_addr | ipv4 %}
{% block maximum_paths %}
address-family ipv4
{% if DEVICE_METADATA['localhost']['type'] == 'ToRRouter' %}
neighbor {{ neighbor_addr }} allowas-in 1
{% endif %}
neighbor {{ neighbor_addr }} activate
neighbor {{ neighbor_addr }} soft-reconfiguration inbound
{% if bgp_session['rrclient'] | int != 0 %}
neighbor {{ neighbor_addr }} route-reflector-client
{% endif %}
{% if bgp_session['nhopself'] | int != 0 %}
neighbor {{ neighbor_addr }} next-hop-self
{% endif %}
maximum-paths 64
exit-address-family
{% endif %}
{% if neighbor_addr | ipv6 %}
address-family ipv6
{% if DEVICE_METADATA['localhost']['type'] == 'ToRRouter' %}
neighbor {{ neighbor_addr }} allowas-in 1
{% endif %}
neighbor {{ neighbor_addr }} activate
neighbor {{ neighbor_addr }} soft-reconfiguration inbound
{% if bgp_session['rrclient'] | int != 0 %}
neighbor {{ neighbor_addr }} route-reflector-client
{% endif %}
{% if bgp_session['nhopself'] | int != 0 %}
neighbor {{ neighbor_addr }} next-hop-self
{% endif %}
{% if bgp_session['asn'] != DEVICE_METADATA['localhost']['bgp_asn'] %}
neighbor {{ neighbor_addr }} route-map set-next-hop-global-v6 in
{% endif %}
{# Apply default route-map for v6 peers #}
neighbor {{ neighbor_addr }} route-map TO_BGP_PEER_V6 out
maximum-paths 64
exit-address-family
{% endblock maximum_paths %}
{% block peers_peer_group %}
neighbor PEER_V4 peer-group
neighbor PEER_V6 peer-group
address-family ipv4
{% if DEVICE_METADATA['localhost']['type'] == 'ToRRouter' %}
neighbor PEER_V4 allowas-in 1
{% endif %}
neighbor PEER_V4 soft-reconfiguration inbound
neighbor PEER_V4 route-map TO_BGP_PEER_V4 out
exit-address-family
address-family ipv6
{% if DEVICE_METADATA['localhost']['type'] == 'ToRRouter' %}
neighbor PEER_V6 allowas-in 1
{% endif %}
{% endfor %}
{% endblock bgp_sessions %}
neighbor PEER_V6 soft-reconfiguration inbound
neighbor PEER_V6 route-map TO_BGP_PEER_V6 out
exit-address-family
{% endblock peers_peer_group %}
{% block bgp_peers_with_range %}
{% if BGP_PEER_RANGE %}
{% for bgp_peer in BGP_PEER_RANGE.values() %}
Expand Down Expand Up @@ -150,11 +119,9 @@ router bgp {{ DEVICE_METADATA['localhost']['bgp_asn'] }}
{% endfor %}
address-family ipv4
neighbor {{ bgp_peer['name'] }} activate
maximum-paths 64
exit-address-family
address-family ipv6
neighbor {{ bgp_peer['name'] }} activate
maximum-paths 64
exit-address-family
{% endfor %}
{% endif %}
Expand Down
Loading