2017-08-01 21:02:00 -05:00
#!/usr/bin/env python
import sys
import subprocess
2019-10-25 15:29:51 -05:00
import datetime
import time
2017-08-01 21:02:00 -05:00
import syslog
2019-10-25 15:29:51 -05:00
import signal
import traceback
import os
import shutil
from collections import defaultdict
from pprint import pprint
2017-08-01 21:02:00 -05:00
2019-10-25 15:29:51 -05:00
import jinja2
import netaddr
from swsscommon import swsscommon
2017-08-08 18:23:58 -05:00
2019-10-25 15:29:51 -05:00
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: "{}", stderr: "{}"'.format(p.returncode, str_cmd, stdout, stderr))
return p.returncode, stdout, stderr
class TemplateFabric(object):
2017-08-08 18:23:58 -05:00
def __init__(self):
2019-10-25 15:29:51 -05:00
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
2017-08-08 18:23:58 -05:00
else:
2019-10-25 15:29:51 -05:00
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.bgp_asn = None
self.meta = None
2020-03-18 10:48:03 -05:00
self.lo_ipv4 = None
self.lo_ipv6 = None
2019-10-31 09:46:17 -05:00
self.neig_meta = {}
2019-10-25 15:29:51 -05:00
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 }}')
self.bgp_peer_shutdown = fabric.from_string('neighbor {{ neighbor_addr }} shutdown')
self.bgp_peer_no_shutdown = fabric.from_string('no neighbor {{ neighbor_addr }} shutdown')
2020-03-18 10:48:03 -05:00
self.zebra_set_src_template = fabric.from_file('zebra.set_src.conf.j2')
2019-10-25 15:29:51 -05:00
daemon.add_manager(swsscommon.CONFIG_DB, swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, self.__metadata_handler)
2019-10-31 09:46:17 -05:00
daemon.add_manager(swsscommon.CONFIG_DB, swsscommon.CFG_DEVICE_NEIGHBOR_METADATA_TABLE_NAME, self.__neighbor_metadata_handler)
2019-10-25 15:29:51 -05:00
daemon.add_manager(swsscommon.CONFIG_DB, swsscommon.CFG_BGP_NEIGHBOR_TABLE_NAME, self.__bgp_handler)
2020-03-18 10:48:03 -05:00
daemon.add_manager(swsscommon.STATE_DB, swsscommon.STATE_INTERFACE_TABLE_NAME, self.__if_handler)
2019-10-25 15:29:51 -05:00
def load_peers(self):
peers = set()
for ip_type in ["ip", "ipv6"]:
command = ["vtysh", "-c", "show %s bgp summary" % ip_type]
rc, out, err = run_command(command)
if rc == 0:
inside = False
for line in out.split("\n"):
if "Neighbor V" in line:
inside = True
elif "Total number of neighbors" in line:
break
elif inside:
if line.startswith("*"):
continue
space = line.find(" ")
if space == -1:
peers.add(line)
else:
peers.add(line[:space])
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
self.meta = { 'localhost': data }
self.bgp_asn = data["bgp_asn"]
self.__update_bgp()
2019-10-31 09:46:17 -05:00
def __neighbor_metadata_handler(self, key, op, data):
if op == swsscommon.SET_COMMAND:
self.neig_meta[key] = data
elif op == swsscommon.DEL_COMMAND:
if key in self.neig_meta:
del self.neig_meta[key]
else:
syslog.syslog(syslog.LOG_ERR,"Can't remove key '%s' from neighbor metadata handler. The key doesn't exist" % key)
else:
syslog.syslog(syslog.LOG_ERR,"Wrong operation '%s' for neighbor metadata handler" % op)
self.__update_bgp()
2020-03-18 10:48:03 -05:00
def __if_handler(self, key, op, data):
cmds = []
if op == swsscommon.SET_COMMAND and key.startswith("Loopback0|") and "state" in data and data["state"] == "ok":
ip_addr_w_mask = key.replace("Loopback0|", "")
slash_pos = ip_addr_w_mask.rfind("/")
if slash_pos == -1:
syslog.syslog(syslog.LOG_ERR, "Wrong Loopback0 ip address'%s'" % ip_addr_w_mask)
return
ip_addr = ip_addr_w_mask[:slash_pos]
try:
if TemplateFabric.is_ipv4(ip_addr) and self.lo_ipv4 is None:
self.lo_ipv4 = ip_addr
txt = self.zebra_set_src_template.render(rm_name="RM_SET_SRC", lo_ip=ip_addr, ip_proto="")
elif TemplateFabric.is_ipv6(ip_addr) and self.lo_ipv6 is None:
self.lo_ipv6 = ip_addr
txt = self.zebra_set_src_template.render(rm_name="RM_SET_SRC6", lo_ip=ip_addr, ip_proto="v6")
else:
syslog.syslog(syslog.LOG_ERR, "Got ambigous ip addres '%s'" % ip_addr)
except:
2020-05-08 01:09:40 -05:00
syslog.syslog(syslog.LOG_ERR, "Error while rendering set src template '%s'" % ip_addr)
2020-03-18 10:48:03 -05:00
else:
cmds.append(txt)
syslog.syslog(syslog.LOG_INFO, "Generate set src configuration with Loopback0 ipv4 '%s'" % ip_addr)
elif op == swsscommon.DEL_COMMAND:
2020-05-08 14:19:48 -05:00
syslog.syslog(syslog.LOG_INFO, "Delete command is not supported for set src templates")
2020-03-18 10:48:03 -05:00
for cmd in cmds:
self.__apply_cmd(cmd, zebra=True)
2019-10-25 15:29:51 -05:00
def __update_bgp(self):
cmds = []
2019-10-31 09:46:17 -05:00
new_bgp_messages = []
2019-10-25 15:29:51 -05:00
for key, op, data in self.bgp_messages:
if op == swsscommon.SET_COMMAND:
if key not in self.peers:
2019-10-31 09:46:17 -05:00
if 'name' in data and data['name'] not in self.neig_meta:
# DEVICE_NEIGHBOR_METADATA should be populated before the rendering
new_bgp_messages.append((key, op, data))
continue
2019-10-25 15:29:51 -05:00
try:
2019-10-31 09:46:17 -05:00
txt = self.bgp_peer_add_template.render(DEVICE_METADATA=self.meta, DEVICE_NEIGHBOR_METADATA=self.neig_meta, neighbor_addr=key, bgp_session=data)
2019-10-25 15:29:51 -05:00
cmds.append(txt)
except:
syslog.syslog(syslog.LOG_ERR, 'Peer {}. Error in rendering the template for "SET" command {}'.format(key, data))
else:
syslog.syslog(syslog.LOG_INFO, 'Peer {} added with attributes {}'.format(key, data))
self.peers.add(key)
else:
# when the peer is already configured we support "shutdown/no shutdown"
# commands for the peers only
if "admin_status" in data:
if data['admin_status'] == 'up':
cmds.append(self.bgp_peer_no_shutdown.render(neighbor_addr=key))
syslog.syslog(syslog.LOG_INFO, 'Peer {} admin state is set to "up"'.format(key))
elif data['admin_status'] == 'down':
cmds.append(self.bgp_peer_shutdown.render(neighbor_addr=key))
syslog.syslog(syslog.LOG_INFO, 'Peer {} admin state is set to "down"'.format(key))
else:
syslog.syslog(syslog.LOG_ERR, "Peer {}: Can't update the peer. has wrong attribute value attr['admin_status'] = '{}'".format(key, data['admin_status']))
else:
syslog.syslog(syslog.LOG_INFO, "Peer {}: Can't update the peer. No 'admin_status' attribute in the request".format(key))
elif op == swsscommon.DEL_COMMAND:
if key in self.peers:
cmds.append(self.bgp_peer_del_template.render(neighbor_addr=key))
syslog.syslog(syslog.LOG_INFO, 'Peer {} has been removed'.format(key))
self.peers.remove(key)
else:
syslog.syslog(syslog.LOG_WARNING, 'Peer {} is not found'.format(key))
2019-10-31 09:46:17 -05:00
self.bgp_messages = new_bgp_messages
2019-10-25 15:29:51 -05:00
for cmd in cmds:
self.__apply_cmd(cmd)
2020-03-18 10:48:03 -05:00
def __apply_cmd(self, cmd, zebra=False):
2019-10-25 15:29:51 -05:00
lines = [line for line in cmd.split("\n") if not line.startswith('!') and line.strip() != ""]
if len(lines) == 0:
return
offset = len(lines[0]) - len(lines[0].lstrip())
chunks = ["dummy"]
for line in cmd.split("\n"):
c_offset = len(line) - len(line.lstrip())
if c_offset > offset:
chunks.append(line.strip())
elif c_offset < offset:
chunks.pop()
chunks.pop()
chunks.append(line.strip())
else:
chunks.pop()
chunks.append(line.strip())
2020-03-18 10:48:03 -05:00
command = ["vtysh", "-c", "conf t"]
if not zebra:
command.append("-c")
command.append("router bgp %s" % self.bgp_asn)
2019-10-25 15:29:51 -05:00
for chunk in chunks:
command.append("-c")
command.append(chunk)
run_command(command)
def __bgp_handler(self, 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 is not None:
self.__update_bgp()
class Daemon(object):
SELECT_TIMEOUT = 1000
2020-03-18 10:48:03 -05:00
DATABASE_LIST = [ swsscommon.CONFIG_DB, swsscommon.STATE_DB ]
2019-10-25 15:29:51 -05:00
def __init__(self):
self.db_connectors = { db : swsscommon.DBConnector(db, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0) for db in Daemon.DATABASE_LIST }
self.selector = swsscommon.Select()
self.callbacks = defaultdict(lambda : defaultdict(list)) # db -> table -> []
self.subscribers = set()
def add_manager(self, db, table_name, callback):
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]:
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 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 not key:
continue
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, dict(fvs))
def wait_for_bgpd():
# wait for 20 seconds
stop_time = datetime.datetime.now() + datetime.timedelta(seconds=20)
syslog.syslog(syslog.LOG_INFO, "Start waiting for bgpd: %s" % str(datetime.datetime.now()))
while datetime.datetime.now() < stop_time:
rc, out, err = run_command(["ps", "ax"])
if rc == 0 and "bgpd" in out:
for line in out.split("\n"):
if "/usr/lib/quagga/bgpd" in line:
time.sleep(0.01) # wait that bgpd connected to vtysh
syslog.syslog(syslog.LOG_INFO, "bgpd connected to vtysh: %s" % str(datetime.datetime.now()))
return
time.sleep(0.1) # sleep 100 ms
raise RuntimeError("bgpd hasn't been started in 20 seconds")
2017-08-08 18:23:58 -05:00
2017-08-01 21:02:00 -05:00
def main():
2019-10-25 15:29:51 -05:00
wait_for_bgpd()
daemon = Daemon()
bgp_manager = BGPConfigManager(daemon)
daemon.run()
def signal_handler(signum, frame):
global g_run
g_run = False
2017-08-08 18:23:58 -05:00
2019-10-25 15:29:51 -05:00
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 RuntimeError as e:
syslog.syslog(syslog.LOG_CRIT, "%s" % str(e))
rc = -2
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)