2019-05-09 01:00:49 -05:00
#!/usr/bin/env python
import sys
import subprocess
2019-10-24 09:35:14 -05:00
import datetime
import time
2019-05-09 01:00:49 -05:00
import syslog
2019-10-24 09:35:14 -05:00
import signal
import traceback
2019-08-29 12:26:48 -05:00
import os
2019-10-24 09:35:14 -05:00
import tempfile
import json
from collections import defaultdict
from pprint import pprint
2019-11-22 13:07:36 -06:00
from pprint import pformat
2019-10-24 09:35:14 -05:00
import jinja2
import netaddr
2019-08-29 12:26:48 -05:00
from swsscommon import swsscommon
2019-05-09 01:00:49 -05:00
2019-08-29 12:26:48 -05:00
2019-10-24 09:35:14 -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()
2019-08-29 12:26:48 -05:00
if p.returncode != 0:
2019-10-24 09:35:14 -05:00
syslog.syslog(syslog.LOG_ERR, 'command execution returned {}. Command: "{}", stdout: "{}", stderr: "{}"'.format(p.returncode, str_cmd, stdout, stderr))
return p.returncode, stdout, stderr
2019-11-22 13:07:36 -06:00
2019-10-24 09:35:14 -05:00
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
2019-08-29 12:26:48 -05:00
class Daemon(object):
SELECT_TIMEOUT = 1000
2019-05-09 01:00:49 -05:00
def __init__(self):
2019-11-22 13:07:36 -06:00
self.db_connectors = {}
2019-08-29 12:26:48 -05:00
self.selector = swsscommon.Select()
2019-10-24 09:35:14 -05:00
self.callbacks = defaultdict(lambda : defaultdict(list)) # db -> table -> []
2019-08-29 12:26:48 -05:00
self.subscribers = set()
def add_manager(self, db, table_name, callback):
2019-11-22 13:07:36 -06:00
if db not in self.db_connectors:
self.db_connectors[db] = swsscommon.DBConnector(db, swsscommon.DBConnector.DEFAULT_UNIXSOCKET, 0)
2019-10-24 09:35:14 -05:00
2019-08-29 12:26:48 -05:00
if table_name not in self.callbacks[db]:
2019-10-24 09:35:14 -05:00
conn = self.db_connectors[db]
2019-08-29 12:26:48 -05:00
subscriber = swsscommon.SubscriberStateTable(conn, table_name)
self.subscribers.add(subscriber)
self.selector.addSelectable(subscriber)
self.callbacks[db][table_name].append(callback)
2019-05-09 01:00:49 -05:00
2019-10-24 09:35:14 -05:00
def run(self):
while g_run:
state, _ = self.selector.select(Daemon.SELECT_TIMEOUT)
if state == self.selector.TIMEOUT:
2019-08-29 12:26:48 -05:00
continue
2019-10-24 09:35:14 -05:00
elif state == self.selector.ERROR:
raise Exception("Received error from select")
2019-08-29 12:26:48 -05:00
for subscriber in self.subscribers:
key, op, fvs = subscriber.pop()
if not key:
continue
2019-10-24 09:35:14 -05:00
if g_debug:
syslog.syslog(syslog.LOG_DEBUG, "Received message : {}".format((key, op, fvs)))
2019-08-29 12:26:48 -05:00
for callback in self.callbacks[subscriber.getDbConnector().getDbId()][subscriber.getTableName()]:
2019-10-24 09:35:14 -05:00
callback(key, op, dict(fvs))
2019-11-22 13:07:36 -06:00
class Directory(object):
def __init__(self):
self.data = defaultdict(dict)
self.notify = defaultdict(lambda: defaultdict(list))
2020-01-03 18:35:08 -06:00
def path_traverse(self, slot, path):
2019-11-22 13:07:36 -06:00
if slot not in self.data:
2020-01-03 18:35:08 -06:00
return False, None
2019-11-22 13:07:36 -06:00
elif path == '':
2020-01-03 18:35:08 -06:00
return True, self.data[slot]
2019-11-22 13:07:36 -06:00
d = self.data[slot]
for p in path.split("/"):
if p not in d:
2020-01-03 18:35:08 -06:00
return False, None
2019-11-22 13:07:36 -06:00
d = d[p]
2020-01-03 18:35:08 -06:00
return True, d
def path_exist(self, slot, path):
return self.path_traverse(slot, path)[0]
2019-11-22 13:07:36 -06:00
def get_path(self, slot, path):
2020-01-03 18:35:08 -06:00
return self.path_traverse(slot, path)[1]
2019-11-22 13:07:36 -06:00
def put(self, slot, key, value):
self.data[slot][key] = value
if slot in self.notify:
for path in self.notify[slot].keys():
if self.path_exist(slot, path):
for handler in self.notify[slot][path]:
handler()
def get(self, slot, key):
return self.data[slot][key]
def remove(self, slot, key):
if slot in self.data:
if key in self.data[slot]:
del self.data[slot][key]
else:
syslog.syslog(syslog.LOG_ERR, "Directory: Can't remove key '%s' from slot '%s'. The key doesn't exist" % (key, slot))
else:
syslog.syslog(syslog.LOG_ERR, "Directory: Can't remove key '%s' from slot '%s'. The slot doesn't exist" % (key, slot))
def remove_slot(self, slot, key):
if slot in self.data:
del self.data[slot]
else:
syslog.syslog(syslog.LOG_ERR, "Directory: Can't remove slot '%s'. The slot doesn't exist" % slot)
def get_slot(self, slot):
return self.data[slot]
def available_slot(self, slot):
return slot in self.data
def available_deps(self, deps):
res = True
for slot, path in deps:
res = res and self.path_exist(slot, path)
return res
def subscribe(self, deps, handler):
for slot, path in deps:
self.notify[slot][path].append(handler)
class Manager(object):
def __init__(self, daemon, directory, deps, database, table_name):
self.directory = directory
self.deps = deps
self.set_queue = []
daemon.add_manager(database, table_name, self.handler)
directory.subscribe(deps, self.on_deps_change)
def handler(self, key, op, data):
if op == swsscommon.SET_COMMAND:
if self.directory.available_deps(self.deps):
res = self.set_handler(key, data)
if not res:
self.set_queue.append((key, data))
else:
self.set_queue.append((key, data))
elif op == swsscommon.DEL_COMMAND:
self.del_handler(key)
else:
syslog.syslog(syslog.LOG_ERR, 'Invalid operation "%s" for key "%s"' % (op, key))
def on_deps_change(self):
new_queue = []
for key, data in self.set_queue:
res = self.set_handler(key, data)
if not res:
new_queue.append((key, data))
self.set_queue = new_queue
def set_handler(self, key, data):
syslog.syslog(syslog.LOG_ERR, "%s wasn't implemented for %s" % (self.__name__, self.__class__))
def del_handler(self, key):
syslog.syslog(syslog.LOG_ERR, "%s wasn't implemented for %s" % (self.__name__, self.__class__))
class BGPDeviceMetaMgr(Manager):
def __init__(self, daemon, directory):
super(BGPDeviceMetaMgr, self).__init__(
daemon,
directory,
[],
swsscommon.CONFIG_DB,
swsscommon.CFG_DEVICE_METADATA_TABLE_NAME
)
def set_handler(self, key, data):
if key != "localhost" or "bgp_asn" not in data:
return
if self.directory.path_exist("meta", "localhost/bgp_asn"):
bgp_asn = self.directory.get_path("meta", "localhost/bgp_asn")
if bgp_asn == data["bgp_asn"]:
return
self.directory.put("meta", key, data)
return True
def del_handler(self, key):
self.directory.remove("meta", key)
class BGPNeighborMetaMgr(Manager):
def __init__(self, daemon, directory):
super(BGPNeighborMetaMgr, self).__init__(
daemon,
directory,
[],
swsscommon.CONFIG_DB,
swsscommon.CFG_DEVICE_NEIGHBOR_METADATA_TABLE_NAME
)
def set_handler(self, key, data):
self.directory.put("neigmeta", key, data)
return True
def del_handler(self, key):
self.directory.remove("neigmeta", key)
class BGPPeerMgr(Manager):
def __init__(self, daemon, directory):
super(BGPPeerMgr, self).__init__(
daemon,
directory,
[
("meta", "localhost/bgp_asn"),
("neigmeta", ""),
],
swsscommon.CONFIG_DB,
swsscommon.CFG_BGP_NEIGHBOR_TABLE_NAME
)
self.peers = self.load_peers()
fabric = TemplateFabric()
self.templates = {
"add": fabric.from_file('bgpd.peer.conf.j2'),
"delete": fabric.from_string('no neighbor {{ neighbor_addr }}'),
"shutdown": fabric.from_string('neighbor {{ neighbor_addr }} shutdown'),
"no shutdown": fabric.from_string('no neighbor {{ neighbor_addr }} shutdown'),
}
def set_handler(self, key, data):
2020-01-03 18:35:08 -06:00
key = self.normalize_key(key)
vrf, nbr = key.split('|', 1)
2019-11-22 13:07:36 -06:00
if key not in self.peers:
cmd = None
neigmeta = self.directory.get_slot("neigmeta")
if 'name' in data and data["name"] not in neigmeta:
return False
try:
cmd = self.templates["add"].render(
DEVICE_METADATA=self.directory.get_slot("meta"),
DEVICE_NEIGHBOR_METADATA=neigmeta,
2020-01-03 18:35:08 -06:00
neighbor_addr=nbr,
2019-11-22 13:07:36 -06:00
bgp_session=data
)
except:
syslog.syslog(syslog.LOG_ERR, 'Peer {}. Error in rendering the template for "SET" command {}'.format(key, data))
return True
if cmd is not None:
2020-01-03 18:35:08 -06:00
rc = self.apply_op(cmd, vrf)
2019-11-22 13:07:36 -06:00
if rc:
self.peers.add(key)
syslog.syslog(syslog.LOG_INFO, 'Peer {} added with attributes {}'.format(key, data))
else:
syslog.syslog(syslog.LOG_ERR, "Peer {} wasn't added.".format(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':
2020-01-03 18:35:08 -06:00
rc = self.apply_op(self.templates["no shutdown"].render(neighbor_addr=nbr), vrf)
2019-11-22 13:07:36 -06:00
if rc:
syslog.syslog(syslog.LOG_INFO, 'Peer {} admin state is set to "up"'.format(key))
else:
syslog.syslog(syslog.LOG_ERR, "Peer {} admin state wasn't set to 'up'.".format(key))
elif data['admin_status'] == 'down':
2020-01-03 18:35:08 -06:00
rc = self.apply_op(self.templates["shutdown"].render(neighbor_addr=nbr), vrf)
2019-11-22 13:07:36 -06:00
if rc:
syslog.syslog(syslog.LOG_INFO, 'Peer {} admin state is set to "down"'.format(key))
else:
syslog.syslog(syslog.LOG_ERR, "Peer {} admin state wasn't 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_ERR, "Peer {}: Can't update the peer. No 'admin_status' attribute in the request".format(key))
return True
def del_handler(self, key):
2020-01-03 18:35:08 -06:00
key = self.normalize_key(key)
vrf, nbr = key.split('|', 1)
2019-11-22 13:07:36 -06:00
if key not in self.peers:
syslog.syslog(syslog.LOG_WARNING, 'Peer {} has not been found'.format(key))
return
2020-01-03 18:35:08 -06:00
cmd = self.templates["delete"].render(neighbor_addr=nbr)
rc = self.apply_op(cmd, vrf)
2019-11-22 13:07:36 -06:00
if rc:
syslog.syslog(syslog.LOG_INFO, 'Peer {} has been removed'.format(key))
self.peers.remove(key)
else:
syslog.syslog(syslog.LOG_ERR, "Peer {} hasn't been removed".format(key))
2020-01-03 18:35:08 -06:00
def apply_op(self, cmd, vrf):
2019-11-22 13:07:36 -06:00
bgp_asn = self.directory.get_slot("meta")["localhost"]["bgp_asn"]
fd, tmp_filename = tempfile.mkstemp(dir='/tmp')
os.close(fd)
with open(tmp_filename, 'w') as fp:
2020-01-03 18:35:08 -06:00
if vrf == 'default':
fp.write('router bgp %s\n' % bgp_asn)
else:
fp.write('router bgp %s vrf %s\n' % (bgp_asn, vrf))
2019-11-22 13:07:36 -06:00
fp.write("%s\n" % cmd)
command = ["vtysh", "-f", tmp_filename]
rc, _, _ = run_command(command)
os.remove(tmp_filename)
return rc == 0
2020-01-03 18:35:08 -06:00
@staticmethod
def normalize_key(key):
if '|' not in key:
return 'default|' + key
else:
return key
2019-11-22 13:07:36 -06:00
@staticmethod
def load_peers():
2020-01-03 18:35:08 -06:00
vrfs = []
command = ["vtysh", "-c", "show bgp vrfs json"]
2019-11-22 13:07:36 -06:00
rc, out, err = run_command(command)
if rc == 0:
2020-01-03 18:35:08 -06:00
js_vrf = json.loads(out)
vrfs = js_vrf['vrfs'].keys()
peers = set()
for vrf in vrfs:
command = ["vtysh", "-c", 'show bgp vrf {} neighbors json'.format(vrf)]
rc, out, err = run_command(command)
if rc == 0:
js_bgp = json.loads(out)
for nbr in js_bgp.keys():
peers.add((vrf, nbr))
2019-11-22 13:07:36 -06:00
return peers
2019-10-24 09:35:14 -05:00
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(["vtysh", "-c", "show daemons"])
if rc == 0 and "bgpd" in out:
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")
2019-05-09 01:00:49 -05:00
def main():
2019-11-22 13:07:36 -06:00
managers = [
BGPDeviceMetaMgr,
BGPNeighborMetaMgr,
BGPPeerMgr,
]
2019-10-24 09:35:14 -05:00
wait_for_bgpd()
2019-08-29 12:26:48 -05:00
daemon = Daemon()
2019-11-22 13:07:36 -06:00
directory = Directory()
manager_instanses = [ manager(daemon, directory) for manager in managers ]
2019-10-24 09:35:14 -05:00
daemon.run()
def signal_handler(signum, frame):
global g_run
g_run = False
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)