2f44bcd071
The one big bgp configuration template was splitted into chunks. Currently we have three types of bgp neighbor peers: general bgp peers. They are represented by CONFIG_DB::BGP_NEIGHBOR table entries dynamic bgp peers. They are represented by CONFIG_DB::BGP_PEER_RANGE table entries monitors bgp peers. They are represented by CONFIG_DB::BGP_MONITORS table entries This PR introduces three templates for each peer type: bgp policies: represent policieas that will be applied to the bgp peer-group (ip prefix-lists, route-maps, etc) bgp peer-group: represent bgp peer group which has common configuration for the bgp peer type and uses bgp routing policy from the previous item bgp peer-group instance: represent bgp configuration, which will be used to instatiate a bgp peer-group for the bgp peer-type. Usually this one is simple, consist of the referral to the bgp peer-group, bgp peer description and bgp peer ip address. This PR redefined constant.yml file. Now this file has a setting for to use or don't use bgp_neighbor metadata. This file has more parameters for now, which are not used. They will be used in the next iteration of bgpcfgd. Currently all tests have been disabled. I'm going to create next PR with the tests right after this PR is merged. I'm going to introduce better bgpcfgd in a short time. It will include support of dynamic changes for the templates. FIX:: #4231
1137 lines
43 KiB
Python
Executable File
1137 lines
43 KiB
Python
Executable File
#!/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
|
|
|
|
import yaml
|
|
import jinja2
|
|
import netaddr
|
|
from swsscommon import swsscommon
|
|
|
|
|
|
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):
|
|
""" This class stores values and notifies callbacks which were registered to be executed as soon
|
|
as some value is changed. This class works as DB cache mostly """
|
|
def __init__(self):
|
|
self.data = defaultdict(dict) # storage. A key is a slot name, a value is a dictionary with data
|
|
self.notify = defaultdict(lambda: defaultdict(list)) # registered callbacks: slot -> path -> handlers[]
|
|
|
|
@staticmethod
|
|
def get_slot_name(db, table):
|
|
""" Convert db, table pair into a slot name """
|
|
return db + "__" + table
|
|
|
|
def path_traverse(self, slot, path):
|
|
"""
|
|
Traverse a path in the storage.
|
|
If the path is an empty string, it returns a value as it is.
|
|
If the path is not an empty string, the method will traverse through the dictionary value.
|
|
Example:
|
|
self.data["key_1"] = { "abc": { "cde": { "fgh": "val_1", "ijk": "val_2" } } }
|
|
self.path_traverse("key_1", "abc/cde") will return True, { "fgh": "val_1", "ijk": "val_2" }
|
|
:param slot: storage key
|
|
:param path: storage path as a string where each internal key is separated by '/'
|
|
:return: a pair: True if the path was found, object if it was found
|
|
"""
|
|
if slot not in self.data:
|
|
return False, None
|
|
elif path == '':
|
|
return True, self.data[slot]
|
|
d = self.data[slot]
|
|
for p in path.split("/"):
|
|
if p not in d:
|
|
return False, None
|
|
d = d[p]
|
|
return True, d
|
|
|
|
def path_exist(self, db, table, path):
|
|
"""
|
|
Check if the path exists in the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
:param path: requested path
|
|
:return: True if the path is available, False otherwise
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
return self.path_traverse(slot, path)[0]
|
|
|
|
def get_path(self, db, table, path):
|
|
"""
|
|
Return the requested path from the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
:param path: requested path
|
|
:return: object if the path was found, None otherwise
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
return self.path_traverse(slot, path)[1]
|
|
|
|
def put(self, db, table, key, value):
|
|
"""
|
|
Put information into the storage. Notify handlers which are dependant to the information
|
|
:param db: db name
|
|
:param table: table name
|
|
:param key: key to change
|
|
:param value: value to put
|
|
:return:
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
self.data[slot][key] = value
|
|
if slot in self.notify:
|
|
for path in self.notify[slot].keys():
|
|
if self.path_exist(db, table, path):
|
|
for handler in self.notify[slot][path]:
|
|
handler()
|
|
|
|
def get(self, db, table, key):
|
|
"""
|
|
Get a value from the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
:param key: ket to get
|
|
:return: value for the key
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
return self.data[slot][key]
|
|
|
|
def get_slot(self, db, table):
|
|
"""
|
|
Get an object from the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
:return: object for the slot
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
return self.data[slot]
|
|
|
|
def remove(self, db, table, key):
|
|
"""
|
|
Remove a value from the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
:param key: key to remove
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
if slot in self.data:
|
|
if key in self.data[slot]:
|
|
del self.data[slot][key]
|
|
else:
|
|
log_err("Directory: Can't remove key '%s' from slot '%s'. The key doesn't exist" % (key, slot))
|
|
else:
|
|
log_err("Directory: Can't remove key '%s' from slot '%s'. The slot doesn't exist" % (key, slot))
|
|
|
|
def remove_slot(self, db, table):
|
|
"""
|
|
Remove an object from the storage
|
|
:param db: db name
|
|
:param table: table name
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
if slot in self.data:
|
|
del self.data[slot]
|
|
else:
|
|
log_err("Directory: Can't remove slot '%s'. The slot doesn't exist" % slot)
|
|
|
|
def available(self, db, table):
|
|
"""
|
|
Check if the table is available
|
|
:param db: db name
|
|
:param table: table name
|
|
:return: True if the slot is available, False if not
|
|
"""
|
|
slot = self.get_slot_name(db, table)
|
|
return slot in self.data
|
|
|
|
def available_deps(self, deps):
|
|
"""
|
|
Check if all items from the deps list is available in the storage
|
|
:param deps: list of dependencies
|
|
:return: True if all dependencies are presented, False otherwise
|
|
"""
|
|
res = True
|
|
for db, table, path in deps:
|
|
res = res and self.path_exist(db, table, path)
|
|
return res
|
|
|
|
def subscribe(self, deps, handler):
|
|
"""
|
|
Subscribe the handler to be run as soon as all dependencies are presented
|
|
:param deps:
|
|
:param handler:
|
|
:return:
|
|
"""
|
|
for db, table, path in deps:
|
|
slot = self.get_slot_name(db, table)
|
|
self.notify[slot][path].append(handler)
|
|
|
|
|
|
class Runner(object):
|
|
""" Implements main io-loop of the application
|
|
It will run event handlers inside of Manager objects
|
|
when corresponding db/table is updated
|
|
"""
|
|
SELECT_TIMEOUT = 1000
|
|
|
|
def __init__(self):
|
|
""" Constructor """
|
|
self.db_connectors = {}
|
|
self.selector = swsscommon.Select()
|
|
self.callbacks = defaultdict(lambda: defaultdict(list)) # db -> table -> handlers[]
|
|
self.subscribers = set()
|
|
|
|
def add_manager(self, manager):
|
|
"""
|
|
Add a manager to the Runner.
|
|
As soon as new events will be receiving by Runner,
|
|
handlers of corresponding objects will be executed
|
|
:param manager: an object implementing Manager
|
|
"""
|
|
db_name = manager.get_database()
|
|
table_name = manager.get_table_name()
|
|
db = swsscommon.SonicDBConfig.getDbId(db_name)
|
|
if db not in self.db_connectors:
|
|
self.db_connectors[db] = swsscommon.DBConnector(db_name, 0)
|
|
|
|
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(manager.handler)
|
|
|
|
def run(self):
|
|
""" Main loop """
|
|
while g_run:
|
|
state, _ = self.selector.select(Runner.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
|
|
log_debug("Received message : '%s'" % str((key, op, fvs)))
|
|
for callback in self.callbacks[subscriber.getDbConnector().getDbId()][subscriber.getTableName()]:
|
|
callback(key, op, dict(fvs))
|
|
|
|
|
|
class Manager(object):
|
|
""" This class represents a SONiC DB table """
|
|
def __init__(self, common_objs, deps, database, table_name):
|
|
"""
|
|
Initialize class
|
|
:param common_objs: common object dictionary
|
|
:param deps: dependencies list
|
|
:param database: database name
|
|
:param table_name: table name
|
|
"""
|
|
self.directory = common_objs['directory']
|
|
self.cfg_mgr = common_objs['cfg_mgr']
|
|
self.constants = common_objs['constants']
|
|
self.deps = deps
|
|
self.db_name = database
|
|
self.table_name = table_name
|
|
self.set_queue = []
|
|
self.directory.subscribe(deps, self.on_deps_change) # subscribe this class method on directory changes
|
|
|
|
def get_database(self):
|
|
""" Return associated database """
|
|
return self.db_name
|
|
|
|
def get_table_name(self):
|
|
""" Return associated table name"""
|
|
return self.table_name
|
|
|
|
def handler(self, key, op, data):
|
|
"""
|
|
This method is executed on each add/remove event on the table.
|
|
:param key: key of the table entry
|
|
:param op: operation on the table entry. Could be either 'SET' or 'DEL'
|
|
:param data: associated data of the event. Empty for 'DEL' operation.
|
|
"""
|
|
if op == swsscommon.SET_COMMAND:
|
|
if self.directory.available_deps(self.deps): # all required dependencies are set in the Directory?
|
|
res = self.set_handler(key, data)
|
|
if not res: # set handler returned False, which means it is not ready to process is. Save it for later.
|
|
log_debug("'SET' handler returned NOT_READY for the Manager: %s" % self.__class__)
|
|
self.set_queue.append((key, data))
|
|
else:
|
|
log_debug("Not all dependencies are met for the Manager: %s" % self.__class__)
|
|
self.set_queue.append((key, data))
|
|
elif op == swsscommon.DEL_COMMAND:
|
|
self.del_handler(key)
|
|
else:
|
|
log_err("Invalid operation '%s' for key '%s'" % (op, key))
|
|
|
|
def on_deps_change(self):
|
|
""" This method is being executed on every dependency change """
|
|
if not self.directory.available_deps(self.deps):
|
|
return
|
|
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):
|
|
""" Placeholder for 'SET' command """
|
|
log_err("set_handler() wasn't implemented for %s" % self.__class__.__name__)
|
|
|
|
def del_handler(self, key):
|
|
""" Placeholder for 'DEL' command """
|
|
log_err("del_handler wasn't implemented for %s" % self.__class__.__name__)
|
|
|
|
|
|
class BGPDataBaseMgr(Manager):
|
|
""" This class updates the Directory object when db table is updated """
|
|
def __init__(self, common_objs, db, table):
|
|
"""
|
|
Initialize the object
|
|
:param common_objs: common object dictionary
|
|
:param db: name of the db
|
|
:param table: name of the table in the db
|
|
"""
|
|
super(BGPDataBaseMgr, self).__init__(
|
|
common_objs,
|
|
[],
|
|
db,
|
|
table,
|
|
)
|
|
|
|
def set_handler(self, key, data):
|
|
""" Implementation of 'SET' command for this class """
|
|
self.directory.put(self.db_name, self.table_name, key, data)
|
|
|
|
return True
|
|
|
|
def del_handler(self, key):
|
|
""" Implementation of 'DEL' command for this class """
|
|
self.directory.remove(self.db_name, self.table_name, key)
|
|
|
|
|
|
class InterfaceMgr(Manager):
|
|
""" This class updates the Directory object when interface-related table is updated """
|
|
def __init__(self, common_objs, db, table):
|
|
"""
|
|
Initialize the object
|
|
:param common_objs: common object dictionary
|
|
:param db: name of the db
|
|
:param table: name of the table in the db
|
|
"""
|
|
super(InterfaceMgr, self).__init__(
|
|
common_objs,
|
|
[],
|
|
db,
|
|
table,
|
|
)
|
|
|
|
def set_handler(self, key, data):
|
|
""" Implementation of 'SET' command.
|
|
Similar to BGPDataBaseMgr but enriches data object with additional data """
|
|
# Interface table can have two keys,
|
|
# one with ip prefix and one without ip prefix
|
|
if '|' in key:
|
|
interface_name, network_str = key.split('|', 1)
|
|
try:
|
|
network = netaddr.IPNetwork(str(network_str))
|
|
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
|
|
log_warn("Subnet '%s' format is wrong for interface '%s'" % (network_str, data["interface"]))
|
|
return True
|
|
data["interface"] = interface_name
|
|
data["prefixlen"] = str(network.prefixlen)
|
|
ip = str(network.ip)
|
|
self.directory.put("LOCAL", "local_addresses", ip, data)
|
|
self.directory.put(self.db_name, self.table_name, key, data)
|
|
self.directory.put("LOCAL", "interfaces", key, data)
|
|
return True
|
|
|
|
def del_handler(self, key):
|
|
""" Implementation of 'DEL' command
|
|
Also removes data object enrichment """
|
|
if '|' in key:
|
|
interface, network = key.split('|', 1)
|
|
try:
|
|
network = netaddr.IPNetwork(str(network))
|
|
except (netaddr.NotRegisteredError, netaddr.AddrFormatError, netaddr.AddrConversionError):
|
|
log_warn("Subnet '%s' format is wrong for interface '%s'" % (network, interface))
|
|
return
|
|
ip = str(network.ip)
|
|
self.directory.remove("LOCAL", "local_addresses", ip)
|
|
self.directory.remove(self.db_name, self.table_name, key)
|
|
self.directory.remove("LOCAL", "interfaces", key)
|
|
|
|
|
|
class BGPPeerGroupMgr(object):
|
|
""" This class represents peer-group and routing policy for the peer_type """
|
|
def __init__(self, common_objs, base_template):
|
|
"""
|
|
Construct the object
|
|
:param common_objs: common objects
|
|
:param base_template: path to the directory with Jinja2 templates
|
|
"""
|
|
self.cfg_mgr = common_objs['cfg_mgr']
|
|
self.constants = common_objs['constants']
|
|
tf = common_objs['tf']
|
|
self.policy_template = tf.from_file(base_template + "policies.conf.j2")
|
|
self.peergroup_template = tf.from_file(base_template + "peer-group.conf.j2")
|
|
|
|
def update(self, name, **kwargs):
|
|
"""
|
|
Update peer-group and routing policy for the peer with the name
|
|
:param name: name of the peer. Used for logging only
|
|
:param kwargs: dictionary with parameters for rendering
|
|
"""
|
|
rc_policy = self.update_policy(name, **kwargs)
|
|
rc_pg = self.update_pg(name, **kwargs)
|
|
return rc_policy and rc_pg
|
|
|
|
def update_policy(self, name, **kwargs):
|
|
"""
|
|
Update routing policy for the peer
|
|
:param name: name of the peer. Used for logging only
|
|
:param kwargs: dictionary with parameters for rendering
|
|
"""
|
|
try:
|
|
policy = self.policy_template.render(**kwargs)
|
|
except jinja2.TemplateError as e:
|
|
log_err("Can't render policy template name: '%s': %s" % (name, str(e)))
|
|
return False
|
|
|
|
return self.update_entity(policy, "Routing policy for peer '%s'" % name)
|
|
|
|
def update_pg(self, name, **kwargs):
|
|
"""
|
|
Update peer-group for the peer
|
|
:param name: name of the peer. Used for logging only
|
|
:param kwargs: dictionary with parameters for rendering
|
|
"""
|
|
try:
|
|
pg = self.peergroup_template.render(**kwargs)
|
|
except jinja2.TemplateError as e:
|
|
log_err("Can't render peer-group template: '%s': %s" % (name, str(e)))
|
|
return False
|
|
|
|
if kwargs['vrf'] == 'default':
|
|
cmd = ('router bgp %s\n' % kwargs['bgp_asn']) + pg
|
|
else:
|
|
cmd = ('router bgp %s vrf %s\n' % (kwargs['bgp_asn'], kwargs['vrf'])) + pg
|
|
|
|
return self.update_entity(cmd, "Peer-group for peer '%s'" % name)
|
|
|
|
def update_entity(self, cmd, txt):
|
|
"""
|
|
Send commands to FRR
|
|
:param cmd: commands to send in a raw form
|
|
:param txt: text for the syslog output
|
|
:return:
|
|
"""
|
|
ret_code = self.cfg_mgr.push(cmd)
|
|
if ret_code:
|
|
log_info("%s was updated" % txt)
|
|
else:
|
|
log_err("Can't update %s" % txt)
|
|
return ret_code
|
|
|
|
|
|
class BGPPeerMgrBase(Manager):
|
|
""" Manager of BGP peers """
|
|
def __init__(self, common_objs, db_name, table_name, peer_type):
|
|
"""
|
|
Initialize the object
|
|
:param common_objs: common objects
|
|
:param table_name: name of the table with peers
|
|
:param peer_type: type of the peers. It is used to find right templates
|
|
"""
|
|
self.common_objs = common_objs
|
|
self.constants = self.common_objs["constants"]
|
|
self.fabric = common_objs['tf']
|
|
self.peer_type = peer_type
|
|
|
|
base_template = "bgpd/templates/" + self.constants["bgp"]["peers"][peer_type]["template_dir"] + "/"
|
|
self.templates = {
|
|
"add": self.fabric.from_file(base_template + "instance.conf.j2"),
|
|
"delete": self.fabric.from_string('no neighbor {{ neighbor_addr }}'),
|
|
"shutdown": self.fabric.from_string('neighbor {{ neighbor_addr }} shutdown'),
|
|
"no shutdown": self.fabric.from_string('no neighbor {{ neighbor_addr }} shutdown'),
|
|
}
|
|
|
|
deps = [
|
|
("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, "localhost/bgp_asn"),
|
|
("CONFIG_DB", swsscommon.CFG_LOOPBACK_INTERFACE_TABLE_NAME, "Loopback0"),
|
|
("LOCAL", "local_addresses", ""),
|
|
("LOCAL", "interfaces", ""),
|
|
]
|
|
|
|
self.check_neig_meta = 'bgp' in self.constants \
|
|
and 'use_neighbors_meta' in self.constants['bgp'] \
|
|
and self.constants['bgp']['use_neighbors_meta']
|
|
self.check_deployment_id = 'bgp' in self.constants \
|
|
and 'use_deployment_id' in self.constants['bgp'] \
|
|
and self.constants['bgp']['use_deployment_id']
|
|
|
|
if self.check_neig_meta:
|
|
deps.append(("CONFIG_DB", swsscommon.CFG_DEVICE_NEIGHBOR_METADATA_TABLE_NAME, ""))
|
|
|
|
if self.check_deployment_id:
|
|
deps.append(("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, "localhost/deployment_id"))
|
|
|
|
super(BGPPeerMgrBase, self).__init__(
|
|
common_objs,
|
|
deps,
|
|
db_name,
|
|
table_name,
|
|
)
|
|
|
|
self.peers = self.load_peers()
|
|
self.peer_group_mgr = BGPPeerGroupMgr(self.common_objs, base_template)
|
|
return
|
|
|
|
def set_handler(self, key, data):
|
|
"""
|
|
It runs on 'SET' command
|
|
:param key: key of the changed table
|
|
:param data: the data associated with the change
|
|
"""
|
|
vrf, nbr = self.split_key(key)
|
|
if key not in self.peers:
|
|
return self.add_peer(vrf, nbr, data)
|
|
else:
|
|
return self.update_peer(vrf, nbr, data)
|
|
|
|
def add_peer(self, vrf, nbr, data):
|
|
"""
|
|
Add a peer into FRR. This is used if the peer is not existed in FRR yet
|
|
:param vrf: vrf name. Name is equal "default" for the global vrf
|
|
:param nbr: neighbor ip address (name for dynamic peer type)
|
|
:param data: associated data
|
|
:return: True if this adding was successful, False otherwise
|
|
"""
|
|
print_data = vrf, nbr, data
|
|
bgp_asn = self.directory.get_slot("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME)["localhost"]["bgp_asn"]
|
|
#
|
|
lo0_ipv4 = self.get_lo0_ipv4()
|
|
if lo0_ipv4 is None:
|
|
log_warn("Loopback0 ipv4 address is not presented yet")
|
|
return False
|
|
#
|
|
if "local_addr" not in data:
|
|
log_warn("Peer %s. Missing attribute 'local_addr'" % nbr)
|
|
else:
|
|
# The bgp session that belongs to a vnet cannot be advertised as the default BGP session.
|
|
# So we need to check whether this bgp session belongs to a vnet.
|
|
interface = self.get_local_interface(data["local_addr"])
|
|
if not interface:
|
|
print_data = nbr, data["local_addr"]
|
|
log_debug("Peer '%s' with local address '%s' wait for the corresponding interface to be set" % print_data)
|
|
return False
|
|
vnet = self.get_vnet(interface)
|
|
if vnet:
|
|
# Ignore the bgp session that is in a vnet
|
|
log_info("Ignore the BGP peer '%s' as the interface '%s' is in vnet '%s'" % (nbr, interface, vnet))
|
|
return True
|
|
|
|
kwargs = {
|
|
'CONFIG_DB__DEVICE_METADATA': self.directory.get_slot("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME),
|
|
'constants': self.constants,
|
|
'bgp_asn': bgp_asn,
|
|
'vrf': vrf,
|
|
'neighbor_addr': nbr,
|
|
'bgp_session': data,
|
|
'loopback0_ipv4': lo0_ipv4,
|
|
}
|
|
if self.check_neig_meta:
|
|
neigmeta = self.directory.get_slot("CONFIG_DB", swsscommon.CFG_DEVICE_NEIGHBOR_METADATA_TABLE_NAME)
|
|
if 'name' in data and data["name"] not in neigmeta:
|
|
log_info("DEVICE_NEIGHBOR_METADATA is not ready for neighbor '%s' - '%s'" % (nbr, data['name']))
|
|
return False
|
|
kwargs['CONFIG_DB__DEVICE_NEIGHBOR_METADATA'] = neigmeta
|
|
|
|
tag = data['name'] if 'name' in data else nbr
|
|
self.peer_group_mgr.update(tag, **kwargs)
|
|
|
|
try:
|
|
cmd = self.templates["add"].render(**kwargs)
|
|
except jinja2.TemplateError as e:
|
|
msg = "Peer '(%s|%s)'. Error in rendering the template for 'SET' command '%s'" % print_data
|
|
log_err("%s: %s" % (msg, str(e)))
|
|
return True
|
|
if cmd is not None:
|
|
ret_code = self.apply_op(cmd, vrf)
|
|
key = (vrf, nbr)
|
|
if ret_code:
|
|
self.peers.add(key)
|
|
log_info("Peer '(%s|%s)' added with attributes '%s'" % print_data)
|
|
else:
|
|
log_err("Peer '(%s|%s)' wasn't added." % (vrf, nbr))
|
|
|
|
return True
|
|
|
|
def update_peer(self, vrf, nbr, data):
|
|
"""
|
|
Update a peer. This is used when the peer is already in the FRR
|
|
Update support only "admin_status" for now
|
|
:param vrf: vrf name. Name is equal "default" for the global vrf
|
|
:param nbr: neighbor ip address (name for dynamic peer type)
|
|
:param data: associated data
|
|
:return: True if this adding was successful, False otherwise
|
|
"""
|
|
if "admin_status" in data:
|
|
self.change_admin_status(vrf, nbr, data)
|
|
else:
|
|
log_err("Peer '(%s|%s)': Can't update the peer. Only 'admin_status' attribute is supported" % (vrf, nbr))
|
|
|
|
return True
|
|
|
|
def change_admin_status(self, vrf, nbr, data):
|
|
"""
|
|
Change admin status of a peer
|
|
:param vrf: vrf name. Name is equal "default" for the global vrf
|
|
:param nbr: neighbor ip address (name for dynamic peer type)
|
|
:param data: associated data
|
|
:return: True if this adding was successful, False otherwise
|
|
"""
|
|
if data['admin_status'] == 'up':
|
|
self.apply_admin_status(vrf, nbr, "no shutdown", "up")
|
|
elif data['admin_status'] == 'down':
|
|
self.apply_admin_status(vrf, nbr, "shutdown", "down")
|
|
else:
|
|
print_data = vrf, nbr, data['admin_status']
|
|
log_err("Peer '%s|%s': Can't update the peer. It has wrong attribute value attr['admin_status'] = '%s'" % print_data)
|
|
|
|
def apply_admin_status(self, vrf, nbr, template_name, admin_state):
|
|
"""
|
|
Render admin state template and apply the command to the FRR
|
|
:param vrf: vrf name. Name is equal "default" for the global vrf
|
|
:param nbr: neighbor ip address (name for dynamic peer type)
|
|
:param template_name: name of the template to render
|
|
:param admin_state: desired admin state
|
|
:return: True if this adding was successful, False otherwise
|
|
"""
|
|
print_data = vrf, nbr, admin_state
|
|
ret_code = self.apply_op(self.templates[template_name].render(neighbor_addr=nbr), vrf)
|
|
if ret_code:
|
|
log_info("Peer '%s|%s' admin state is set to '%s'" % print_data)
|
|
else:
|
|
log_err("Can't set peer '%s|%s' admin state to '%s'." % print_data)
|
|
|
|
def del_handler(self, key):
|
|
"""
|
|
'DEL' handler for the BGP PEER tables
|
|
:param key: key of the neighbor
|
|
"""
|
|
vrf, nbr = self.split_key(key)
|
|
if key not in self.peers:
|
|
log_warn("Peer '(%s|%s)' has not been found" % (vrf, nbr))
|
|
return
|
|
cmd = self.templates["delete"].render(neighbor_addr=nbr)
|
|
ret_code = self.apply_op(cmd, vrf)
|
|
if ret_code:
|
|
log_info("Peer '(%s|%s)' has been removed" % (vrf, nbr))
|
|
self.peers.remove(key)
|
|
else:
|
|
log_err("Peer '(%s|%s)' hasn't been removed" % (vrf, nbr))
|
|
|
|
def apply_op(self, cmd, vrf):
|
|
"""
|
|
Push commands cmd into FRR
|
|
:param cmd: commands in raw format
|
|
:param vrf: vrf where the commands should be applied
|
|
:return: True if no errors, False if there are errors
|
|
"""
|
|
bgp_asn = self.directory.get_slot("CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME)["localhost"]["bgp_asn"]
|
|
if vrf == 'default':
|
|
cmd = ('router bgp %s\n' % bgp_asn) + cmd
|
|
else:
|
|
cmd = ('router bgp %s vrf %s\n' % (bgp_asn, vrf)) + cmd
|
|
return self.cfg_mgr.push(cmd)
|
|
|
|
def get_lo0_ipv4(self):
|
|
"""
|
|
Extract Loopback0 ipv4 address from the Directory
|
|
:return: ipv4 address for Loopback0, None if nothing found
|
|
"""
|
|
loopback0_ipv4 = None
|
|
for loopback in self.directory.get_slot("CONFIG_DB", swsscommon.CFG_LOOPBACK_INTERFACE_TABLE_NAME).iterkeys():
|
|
if loopback.startswith("Loopback0|"):
|
|
loopback0_prefix_str = loopback.replace("Loopback0|", "")
|
|
loopback0_ip_str = loopback0_prefix_str[:loopback0_prefix_str.find('/')]
|
|
if TemplateFabric.is_ipv4(loopback0_ip_str):
|
|
loopback0_ipv4 = loopback0_ip_str
|
|
break
|
|
|
|
return loopback0_ipv4
|
|
|
|
def get_local_interface(self, local_addr):
|
|
"""
|
|
Get interface according to the local address from the directory
|
|
:param: directory: Directory object that stored metadata of interfaces
|
|
:param: local_addr: Local address of the interface
|
|
:return: Return the metadata of the interface with the local address
|
|
If the interface has not been set, return None
|
|
"""
|
|
local_addresses = self.directory.get_slot("LOCAL", "local_addresses")
|
|
# Check if the local address of this bgp session has been set
|
|
if local_addr not in local_addresses:
|
|
return None
|
|
local_address = local_addresses[local_addr]
|
|
interfaces = self.directory.get_slot("LOCAL", "interfaces")
|
|
# Check if the information for the interface of this local address has been set
|
|
if local_address.has_key("interface") and local_address["interface"] in interfaces:
|
|
return interfaces[local_address["interface"]]
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_vnet(interface):
|
|
"""
|
|
Get the VNet name of the interface
|
|
:param: interface: The metadata of the interface
|
|
:return: Return the vnet name of the interface if this interface belongs to a vnet,
|
|
Otherwise return None
|
|
"""
|
|
if interface.has_key("vnet_name") and interface["vnet_name"]:
|
|
return interface["vnet_name"]
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def split_key(key):
|
|
"""
|
|
Split key into ip address and vrf name. If there is no vrf, "default" would be return for vrf
|
|
:param key: key to split
|
|
:return: vrf name extracted from the key, peer ip address extracted from the key
|
|
"""
|
|
if '|' not in key:
|
|
return 'default', key
|
|
else:
|
|
return tuple(key.split('|', 1))
|
|
|
|
@staticmethod
|
|
def load_peers():
|
|
"""
|
|
Load peers from FRR.
|
|
:return: set of peers, which are already installed in FRR
|
|
"""
|
|
command = ["vtysh", "-c", "show bgp vrfs json"]
|
|
ret_code, out, err = run_command(command)
|
|
if ret_code == 0:
|
|
js_vrf = json.loads(out)
|
|
vrfs = js_vrf['vrfs'].keys()
|
|
else:
|
|
log_crit("Can't read bgp vrfs: %s" % err)
|
|
raise Exception("Can't read bgp vrfs: %s" % err)
|
|
peers = set()
|
|
for vrf in vrfs:
|
|
command = ["vtysh", "-c", 'show bgp vrf %s neighbors json' % str(vrf)]
|
|
ret_code, out, err = run_command(command)
|
|
if ret_code == 0:
|
|
js_bgp = json.loads(out)
|
|
for nbr in js_bgp.keys():
|
|
peers.add((vrf, nbr))
|
|
else:
|
|
log_crit("Can't read vrf '%s' neighbors: %s" % (vrf, str(err)))
|
|
raise Exception("Can't read vrf '%s' neighbors: %s" % (vrf, str(err)))
|
|
|
|
return peers
|
|
|
|
|
|
class ZebraSetSrc(Manager):
|
|
""" This class initialize "set src" settings for zebra """
|
|
def __init__(self, common_objs, db, table):
|
|
"""
|
|
Initialize the object
|
|
:param common_objs: common object dictionary
|
|
:param db: name of the db
|
|
:param table: name of the table in the db
|
|
"""
|
|
super(ZebraSetSrc, self).__init__(
|
|
common_objs,
|
|
[],
|
|
db,
|
|
table,
|
|
)
|
|
tf = common_objs['tf']
|
|
self.zebra_set_src_template = tf.from_file("zebra/zebra.set_src.conf.j2")
|
|
self.lo_ipv4 = None
|
|
self.lo_ipv6 = None
|
|
|
|
def set_handler(self, key, data):
|
|
""" Implementation of 'SET' command for this class """
|
|
self.directory.put(self.db_name, self.table_name, key, data)
|
|
#
|
|
if 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:
|
|
log_err("Wrong Loopback0 ip address: '%s'" % ip_addr_w_mask)
|
|
return True
|
|
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:
|
|
log_err("Got ambiguous ip address '%s'" % ip_addr)
|
|
return True
|
|
except jinja2.TemplateError as e:
|
|
log_err("Error while rendering 'set src' template: %s" % str(e))
|
|
return True
|
|
if self.cfg_mgr.push(txt):
|
|
log_info("The 'set src' configuration with Loopback0 ip '%s' was pushed" % ip_addr)
|
|
else:
|
|
log_err("The 'set src' configuration with Loopback0 ip '%s' wasn't pushed" % ip_addr)
|
|
return True
|
|
|
|
def del_handler(self, key):
|
|
""" Implementation of 'DEL' command for this class """
|
|
self.directory.remove(self.db_name, self.table_name, key)
|
|
log_warn("Delete command is not supported for 'zebra set src' templates")
|
|
|
|
|
|
def wait_for_daemons(daemons, seconds):
|
|
"""
|
|
Wait until FRR daemons are ready for requests
|
|
:param daemons: list of FRR daemons to wait
|
|
:param seconds: number of seconds to wait, until raise an error
|
|
"""
|
|
stop_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds)
|
|
log_info("Start waiting for FRR daemons: %s" % str(datetime.datetime.now()))
|
|
while datetime.datetime.now() < stop_time:
|
|
ret_code, out, err = run_command(["vtysh", "-c", "show daemons"], hide_errors=True)
|
|
if ret_code == 0 and all(daemon in out for daemon in daemons):
|
|
log_info("All required daemons have connected to vtysh: %s" % str(datetime.datetime.now()))
|
|
return
|
|
else:
|
|
log_warn("Can't read daemon status from FRR: %s" % str(err))
|
|
time.sleep(0.1) # sleep 100 ms
|
|
raise RuntimeError("FRR daemons hasn't been started in %d seconds" % seconds)
|
|
|
|
|
|
def read_constants():
|
|
""" Read file with constants values from /etc/sonic/constants.yml """
|
|
with open('/etc/sonic/constants.yml') as fp:
|
|
content = yaml.load(fp)
|
|
if "constants" not in content:
|
|
log_crit("/etc/sonic/constants.yml doesn't have 'constants' key")
|
|
raise Exception("/etc/sonic/constants.yml doesn't have 'constants' key")
|
|
return content["constants"]
|
|
|
|
|
|
def main():
|
|
""" Main function """
|
|
wait_for_daemons(["bgpd", "zebra", "staticd"], seconds=20)
|
|
#
|
|
common_objs = {
|
|
'directory': Directory(),
|
|
'cfg_mgr': ConfigMgr(),
|
|
'tf': TemplateFabric(),
|
|
'constants': read_constants(),
|
|
}
|
|
managers = [
|
|
# Config DB managers
|
|
BGPDataBaseMgr(common_objs, "CONFIG_DB", swsscommon.CFG_DEVICE_METADATA_TABLE_NAME),
|
|
BGPDataBaseMgr(common_objs, "CONFIG_DB", swsscommon.CFG_DEVICE_NEIGHBOR_METADATA_TABLE_NAME),
|
|
# Interface managers
|
|
InterfaceMgr(common_objs, "CONFIG_DB", swsscommon.CFG_INTF_TABLE_NAME),
|
|
InterfaceMgr(common_objs, "CONFIG_DB", swsscommon.CFG_LOOPBACK_INTERFACE_TABLE_NAME),
|
|
InterfaceMgr(common_objs, "CONFIG_DB", swsscommon.CFG_VLAN_INTF_TABLE_NAME),
|
|
InterfaceMgr(common_objs, "CONFIG_DB", swsscommon.CFG_LAG_INTF_TABLE_NAME),
|
|
# State DB managers
|
|
ZebraSetSrc(common_objs, "STATE_DB", swsscommon.STATE_INTERFACE_TABLE_NAME),
|
|
# Peer Managers
|
|
BGPPeerMgrBase(common_objs, "CONFIG_DB", swsscommon.CFG_BGP_NEIGHBOR_TABLE_NAME, "general"),
|
|
BGPPeerMgrBase(common_objs, "CONFIG_DB", "BGP_MONITORS", "monitors"),
|
|
BGPPeerMgrBase(common_objs, "CONFIG_DB", "BGP_PEER_RANGE", "dynamic"),
|
|
]
|
|
runner = Runner()
|
|
for mgr in managers:
|
|
runner.add_manager(mgr)
|
|
runner.run()
|
|
|
|
|
|
def signal_handler(_, __): # signal_handler(signum, frame)
|
|
""" signal handler """
|
|
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:
|
|
log_notice("Keyboard interrupt")
|
|
except RuntimeError as exc:
|
|
log_crit(str(exc))
|
|
rc = -2
|
|
if g_debug:
|
|
raise
|
|
except Exception as exc:
|
|
log_crit("Got an exception %s: Traceback: %s" % (str(exc), traceback.format_exc()))
|
|
rc = -1
|
|
if g_debug:
|
|
raise
|
|
finally:
|
|
syslog.closelog()
|
|
try:
|
|
sys.exit(rc)
|
|
except SystemExit:
|
|
os._exit(rc)
|