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

Add SRv6 support in Bgpcfgd #21156

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/sonic-bgpcfgd/bgpcfgd/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .managers_rm import RouteMapMgr
from .managers_device_global import DeviceGlobalCfgMgr
from .managers_chassis_app_db import ChassisAppDbMgr
from .managers_srv6 import SRv6Mgr
from .static_rt_timer import StaticRouteTimer
from .runner import Runner, signal_handler
from .template import TemplateFabric
Expand Down Expand Up @@ -75,6 +76,8 @@ def do_work():
RouteMapMgr(common_objs, "APPL_DB", swsscommon.APP_BGP_PROFILE_TABLE_NAME),
# Device Global Manager
DeviceGlobalCfgMgr(common_objs, "CONFIG_DB", swsscommon.CFG_BGP_DEVICE_GLOBAL_TABLE_NAME),
# SRv6 Manager
SRv6Mgr(common_objs, "CONFIG_DB", "SRV6_MY_SID_TABLE")
]

if device_info.is_chassis():
Expand Down
129 changes: 129 additions & 0 deletions src/sonic-bgpcfgd/bgpcfgd/managers_srv6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from .log import log_err, log_debug, log_warn
from .manager import Manager
from swsscommon import swsscommon
from ipaddress import IPv6Address

supported_SRv6_behaviors = {
'uN',
'uDT46',
}

DEFAULT_VRF = "default"

class SRv6Mgr(Manager):
""" This class updates SRv6 configurations when SRV6_MY_SID_TABLE 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(SRv6Mgr, self).__init__(
common_objs,
[],
db,
table,
)

self.sids = {} # locators -> func_bits -> SIDs
self.config_db = swsscommon.SonicV2Connector()
self.config_db.connect(self.config_db.CONFIG_DB)

def set_handler(self, key, data):
ip_addr = key
if 'action' not in data:
log_err("Found a SRv6 config entry that does not specify action: {} | {}".format(ip_addr, data))
return False

if data['action'] not in supported_SRv6_behaviors:
log_err("Found a SRv6 config entry associated with unsupported action: {} | {}".format(ip_addr, data))
return False

sid = SID(ip_addr, data) # the information in data will be parsed into SID's attributes
locator = sid.get_locator()
func_bits = sid.get_func_bits()

cmd_list = ['segment-routing', 'srv6']
cmd_list += ['locators', 'locator {}'.format(locator)]
if locator not in self.sids:
cmd_list += ['prefix {}/{} block-len {} node-len {} func-bits {}'.format(locator, sid.block_len + sid.node_len, sid.block_len, sid.node_len, sid.func_len)]

sid_cmd = 'sid {}/{} {}'.format(ip_addr, sid.block_len + sid.node_len + sid.func_len, sid.action)
if sid.vrf != DEFAULT_VRF:
sid_cmd += ' vrf {}'.format(sid.vrf)
cmd_list.append(sid_cmd)

self.cfg_mgr.push_list(cmd_list)
log_debug("{} SRv6 static configuration {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list)))

self.sids.setdefault(locator, {})[func_bits] = sid
return True

def del_handler(self, key, data):
ip_addr = key
sid = SID(ip_addr, data)
locator = sid.get_locator()
func_bits = sid.get_func_bits()

if locator in self.sids:
if func_bits not in self.sids[locator]:
log_warn("Encountered a config deletion with an unexpected SRv6 action: {} | {}".format(ip_addr, data))
return

cmd_list = ['segment-routing', 'srv6']
cmd_list.append('locators')
if len(self.sids[locator] == 1):
# this is the last func_bits of the locator, so we should delete the whole locator
cmd_list.append('no locator {}'.format(locator))

self.sids.pop(locator)
else:
# delete this func_bits only
cmd_list.append('locator {}'.format(locator))
no_sid_cmd = 'no sid {}/{} {}'.format(ip_addr, sid.block_len + sid.node_len + sid.func_len, sid.action)
if sid.vrf != DEFAULT_VRF:
no_sid_cmd += ' vrf {}'.format(sid.vrf)
cmd_list.append(no_sid_cmd)

self.sids[locator].pop(func_bits)

self.cfg_mgr.push_list(cmd_list)
log_debug("{} SRv6 static configuration {} is scheduled for updates. {}".format(self.db_name, key, str(cmd_list)))
else:
log_warn("Encountered a config deletion with an unexpected SRv6 locator: {} | {}".format(ip_addr, data))
return

class SID:
def __init__(self, ip_addr, data):
self.bits = int(IPv6Address(ip_addr))
self.block_len = data['block_len'] if 'block_len' in data else 32
self.node_len = data['node_len'] if 'node_len' in data else 16
self.func_len = data['func_len'] if 'func_len' in data else 16
self.arg_len = data['arg_len'] if 'arg_len' in data else 0

# extract the locator(block id + node id) embedded in the SID
locator_mask = 0
for i in range(self.block_len + self.node_len):
locator_mask <<= 1
locator_mask |= 0x01
locator_mask <<= 128 - (self.block_len + self.node_len)
self.locator = IPv6Address(self.bits & locator_mask)

# extract the func_bits (function id)
func_mask = 0
for i in range(self.func_len):
func_mask <<= 1
func_mask != 0x01
func_mask <<= 128 - (self.block_len + self.node_len + self.func_len)
self.func_bits = IPv6Address(self.bits & func_mask)

self.action = data['action']
self.vrf = data['vrf'] if 'vrf' in data else DEFAULT_VRF
self.adj = data['adj'].split(',') if 'adj' in data else []

def get_locator(self):
return self.locator

def get_func_bits(self):
return self.func_bits
126 changes: 126 additions & 0 deletions src/sonic-bgpcfgd/tests/test_srv6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from unittest.mock import MagicMock, patch

from bgpcfgd.directory import Directory
from bgpcfgd.template import TemplateFabric
from bgpcfgd.managers_srv6 import SRv6Mgr
from swsscommon import swsscommon

def constructor():
cfg_mgr = MagicMock()

common_objs = {
'directory': Directory(),
'cfg_mgr': cfg_mgr,
'tf': TemplateFabric(),
'constants': {},
}

mgr = SRv6Mgr(common_objs, "CONFIG_DB", "SRV6_MY_SID_TABLE")
assert len(mgr.sids) == 0

return mgr

def op_test(mgr: SRv6Mgr, op, args, expected_ret, expected_cmds):
op_test.push_list_called = False
def push_list_checker(cmds):
op_test.push_list_called = True
assert len(cmds) == len(expected_cmds)
for i in range(len(expected_cmds)):
assert cmds[i] == expected_cmds[i]
return True
mgr.cfg_mgr.push_list = push_list_checker

if op == 'SET':
ret = mgr.set_handler(*args)
mgr.cfg_mgr.push_list = MagicMock()
assert expected_ret == ret
elif op == 'DEL':
mgr.del_handler(*args)
mgr.cfg_mgr.push_list = MagicMock()
else:
mgr.cfg_mgr.push_list = MagicMock()
assert False, "Unexpected operation {}".format(op)

if expected_cmds:
assert op_test.push_list_called, "cfg_mgr.push_list wasn't called"
else:
assert not op_test.push_list_called, "cfg_mgr.push_list was called"

def test_uN_add():
mgr = constructor()

op_test(mgr, 'SET', ("FCBB:BBBB:20:F1::", {
'action': 'uN'
}), expected_ret=True, expected_cmds=[
'segment-routing',
'srv6',
'locators',
'locator FCBB:BBBB:20:: block-len 32 node-len 16 func-bits 16',
'prefix FCBB:BBBB:20::/48',
'sid FCBB:BBBB:20:F1::/64 uN'
])

def test_uDT46_add_vrf1():
mgr = constructor()

_old_exists = swsscommon.SonicV2Connector().exists
swsscommon.SonicV2Connector().exists = lambda x: True

op_test(mgr, 'SET', ("FCBB:BBBB:20:F2::", {
'action': 'uDT46',
'vrf': 'vrf1'
}), expected_ret=True, expected_cmds=[
'segment-routing',
'srv6',
'locators',
'locator FCBB:BBBB:20:: block-len 32 node-len 16 func-bits 16',
'prefix FCBB:BBBB:20::/48',
'sid FCBB:BBBB:20:F2::/64 uDT46 vrf vrf1'
])
swsscommon.SonicV2Connector().exists = _old_exists

def test_uN_del():
mgr = constructor()

# add uN function first
mgr.set_handler("FCBB:BBBB:20:F1::", {
'action': 'uN'
})

# test the deletion
op_test(mgr, 'DEL', ("FCBB:BBBB:20:F1::", {
'action': 'uN'
}), expected_ret=True, expected_cmds=[
'segment-routing',
'srv6',
'locators',
'no locator FCBB:BBBB:20:: block-len 32 node-len 16 func-bits 16'
])

def test_uDT46_del_vrf1():
mgr = constructor()

# add a uN action first to make the uDT46 action not the last function
mgr.set_handler("FCBB:BBBB:20:F1::", {
'action': 'uN'
})

# add the uDT46 action
mgr.set_handler("FCBB:BBBB:20:F2::", {
'action': 'uDT46'
})

# test the deletion of uDT46
_old_exists = swsscommon.SonicV2Connector().exists
swsscommon.SonicV2Connector().exists = lambda x: True
op_test(mgr, 'DEL', ("FCBB:BBBB:20:F2::", {
'action': 'uDT46',
"vrf": "vrf1"
}), expected_ret=True, expected_cmds=[
'segment-routing',
'srv6',
'locators',
'locator FCBB:BBBB:20:: block-len 32 node-len 16 func-bits 16',
'no sid FCBB:BBBB:20:F2/64 uDT46 vrf vrf1'
])
swsscommon.SonicV2Connector().exists = _old_exists
Loading