From 80a4db64d981a8bba685112334f1b8a06ea9dc34 Mon Sep 17 00:00:00 2001 From: Changjie Gao Date: Wed, 11 Jun 2025 10:14:30 +0800 Subject: [PATCH] Add local_add_path parameter --- yabgp/config.py | 13 +- yabgp/core/protocol.py | 36 +- yabgp/message/open.py | 25 +- .../unit/message/attribute/test_add_path.py | 328 ++++++++++++++++++ yabgp/tests/unit/message/test_open.py | 2 +- 5 files changed, 379 insertions(+), 25 deletions(-) create mode 100644 yabgp/tests/unit/message/attribute/test_add_path.py diff --git a/yabgp/config.py b/yabgp/config.py index d198367..09c0dd9 100644 --- a/yabgp/config.py +++ b/yabgp/config.py @@ -41,9 +41,6 @@ cfg.BoolOpt('enhanced_route_refresh', default=True, help='If support enhanced route refresh'), - cfg.StrOpt('add_path', - choices=['ipv4_send', 'ipv4_receive', 'ipv4_both'], - help='BGP additional path feature and supported address family'), cfg.BoolOpt('graceful_restart', default=True, help='if support graceful restart'), @@ -80,7 +77,10 @@ 'rib', default=False, help='maintain rib in or not, default is False' - ) + ), + cfg.ListOpt('local_add_path', + default=['ipv4_both'], + help='The Local BGP add path configuration') ] CONF.register_cli_opts(BGP_PEER_CONFIG_OPTS, group='bgp') @@ -134,7 +134,10 @@ def get_bgp_config(): 'enhanced_route_refresh': CONF.bgp.enhanced_route_refresh, 'graceful_restart': CONF.bgp.graceful_restart, 'cisco_multi_session': CONF.bgp.cisco_multi_session, - 'add_path': CONF.bgp.add_path + 'add_path': [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in CONF.bgp.local_add_path + ] }, 'remote': {} } diff --git a/yabgp/core/protocol.py b/yabgp/core/protocol.py index 0af071a..a7b5a7e 100644 --- a/yabgp/core/protocol.py +++ b/yabgp/core/protocol.py @@ -58,6 +58,7 @@ def __init__(self): self.adj_rib_in = {k: {} for k in CONF.bgp.afi_safi} self.adj_rib_out = {k: {} for k in CONF.bgp.afi_safi} self.adj_rib_in_ipv4_tree = Radix() + self.afi_add_path = None # statistic self.msg_sent_stat = { @@ -263,7 +264,7 @@ def _update_received(self, timestamp, msg): """Called when a BGP Update message was received.""" # TODO: Need to convert `self.add_path_ipv4_receive` and `self.add_path_ipv4_send` into a unified # `afi_add_path` format. - result = Update().parse(timestamp, msg, self.fourbytesas, afi_add_path={}) + result = Update().parse(timestamp, msg, self.fourbytesas, afi_add_path=self.afi_add_path) if result['sub_error']: msg = { 'attr': result['attr'], @@ -470,11 +471,9 @@ def send_open(self): # check add path feature, send add path condition: # local support send or both # remote support receive or both - if cfg.CONF.bgp.running_config['capability']['local']['add_path'] in \ - ['ipv4_send', 'ipv4_both']: - if cfg.CONF.bgp.running_config['capability']['remote'].get('add_path') in \ - ['ipv4_receive', 'ipv4_both']: - self.add_path_ipv4_send = True + local_add_path = cfg.CONF.bgp.running_config['capability']['local']['add_path'] + remote_add_path = cfg.CONF.bgp.running_config['capability']['remote'].get('add_path') + self.afi_add_path = self.compare_add_path(local_add_path, remote_add_path) # send message self.transport.write(open_msg) self.msg_sent_stat['Opens'] += 1 @@ -519,11 +518,9 @@ def _open_received(self, timestamp, msg): if key == 'four_bytes_as': self.fourbytesas = True elif key == 'add_path': - if cfg.CONF.bgp.running_config['capability']['remote']['add_path'] in \ - ['ipv4_send', 'ipv4_both']: - if cfg.CONF.bgp.running_config['capability']['local']['add_path'] in \ - ['ipv4_receive', 'ipv4_both']: - self.add_path_ipv4_receive = True + self.afi_add_path = self.compare_add_path( + cfg.CONF.bgp.running_config['capability']['local']['add_path'], + cfg.CONF.bgp.running_config['capability']['remote'].get('add_path')) LOG.info("--%s = %s", key, cfg.CONF.bgp.running_config['capability']['remote'][key]) @@ -853,3 +850,20 @@ def update_receive_verion(self, attr, nlri, withdraw): del self.mpls_vpn_receive_dict[str(key)] else: LOG.info("Do not have %s in receive mpls_vpn dict" % key) + + def compare_add_path(self, local_add_path, remote_add_path): + if not local_add_path or not remote_add_path: + return None + + afi_add_path = {} + for local in local_add_path: + for remote in remote_add_path: + if local.get('afi_safi') == remote.get('afi_safi'): + local_send_receive = local.get('send/receive') + remote_send_receive = remote.get('send/receive') + if local_send_receive in ['receive', 'both'] and remote_send_receive in ['send', 'both']: + afi_add_path[local.get('afi_safi')] = True + else: + afi_add_path[local.get('afi_safi')] = False + + return afi_add_path diff --git a/yabgp/message/open.py b/yabgp/message/open.py index 8c661f2..c2f07e4 100644 --- a/yabgp/message/open.py +++ b/yabgp/message/open.py @@ -20,6 +20,7 @@ from yabgp.common import exception as excp from yabgp.common import constants as bgp_cons +from yabgp.common.constants import AFI_SAFI_STR_DICT class Open(object): @@ -420,9 +421,13 @@ def construct(self, my_capability=None): return afisafi # for add path elif self.capa_code == self.ADD_PATH: - afi_safi, value = convert_addpath_str_to_int(self.capa_value) + addpath_int_list = convert_addpath_str_to_int(self.capa_value) + length = len(addpath_int_list) * 4 add_path = struct.pack( - '!BBBBHBB', 2, 6, self.ADD_PATH, self.capa_length, afi_safi[0], afi_safi[1], value) + '!BBBB', 2, length + 1 + 1, self.ADD_PATH, length) + for (afi_safi, value) in addpath_int_list: + afi, safi = afi_safi + add_path += struct.pack('!HBB', afi, safi, value) return add_path # (10) Extended Next Hop Encoding Capability @@ -445,10 +450,14 @@ def construct(self, my_capability=None): return struct.pack('!BBBB', 2, 2, self.ENHANCED_ROUTE_REFRESH, 0) -def convert_addpath_str_to_int(addpath_str): - addpath_dict = { - 'ipv4_receive': [(1, 1), 1], - 'ipv4_send': [(1, 1), 2], - 'ipv4_both': [(1, 1), 3] +def convert_addpath_str_to_int(addpath_list): + addpath_int_list = [] + send_receive_dict = { + 'receive': 1, + 'send': 2, + 'both': 3 } - return addpath_dict[addpath_str] + for addpath in addpath_list: + addpath_int_list.append([AFI_SAFI_STR_DICT[addpath['afi_safi']], send_receive_dict[addpath['send/receive']]]) + + return addpath_int_list diff --git a/yabgp/tests/unit/message/attribute/test_add_path.py b/yabgp/tests/unit/message/attribute/test_add_path.py new file mode 100644 index 0000000..4abdea4 --- /dev/null +++ b/yabgp/tests/unit/message/attribute/test_add_path.py @@ -0,0 +1,328 @@ +# Copyright 2015 Cisco Systems, Inc. +# All rights reserved. +# +# Licensed under the Apache License, version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Test Add Path Capability""" + +import unittest +from yabgp.message.open import convert_addpath_str_to_int + + +class TestAddPathCapability(unittest.TestCase): + """ + Test cases for Add Path capability introduced in commits 1a17665 and 814598b. + These tests cover: + 1. convert_addpath_str_to_int function + 2. Add path capability parsing and construction + 3. Add path negotiation logic + """ + + def setUp(self): + """Set up test fixtures""" + self.maxDiff = None + + def test_convert_addpath_str_to_int_single_receive(self): + """ + Test converting a single AFI/SAFI in receive mode + """ + addpath_list = [ + {'afi_safi': 'ipv4', 'send/receive': 'receive'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 1), 1]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_single_send(self): + """ + Test converting a single AFI/SAFI in send mode + """ + addpath_list = [ + {'afi_safi': 'ipv4', 'send/receive': 'send'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 1), 2]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_single_both(self): + """ + Test converting a single AFI/SAFI in both mode + """ + addpath_list = [ + {'afi_safi': 'ipv4', 'send/receive': 'both'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 1), 3]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_multiple_afi_safi(self): + """ + Test converting multiple AFI/SAFI entries + """ + addpath_list = [ + {'afi_safi': 'ipv4', 'send/receive': 'both'}, + {'afi_safi': 'ipv6', 'send/receive': 'receive'}, + {'afi_safi': 'vpnv4', 'send/receive': 'send'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [ + [(1, 1), 3], # ipv4, both + [(2, 1), 1], # ipv6, receive + [(1, 128), 2] # vpnv4, send + ] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_ipv4_lu(self): + """ + Test converting IPv4 Labeled Unicast + """ + addpath_list = [ + {'afi_safi': 'ipv4_lu', 'send/receive': 'both'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 4), 3]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_ipv6_lu(self): + """ + Test converting IPv6 Labeled Unicast + """ + addpath_list = [ + {'afi_safi': 'ipv6_lu', 'send/receive': 'receive'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(2, 4), 1]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_vpnv6(self): + """ + Test converting VPNv6 + """ + addpath_list = [ + {'afi_safi': 'vpnv6', 'send/receive': 'both'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(2, 128), 3]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_flowspec(self): + """ + Test converting Flowspec + """ + addpath_list = [ + {'afi_safi': 'flowspec', 'send/receive': 'send'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 133), 2]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_empty_list(self): + """ + Test converting empty list + """ + addpath_list = [] + result = convert_addpath_str_to_int(addpath_list) + expected = [] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_mixed_families(self): + """ + Test converting mixed address families (IPv4, IPv6, VPN, Labeled Unicast) + """ + addpath_list = [ + {'afi_safi': 'ipv4', 'send/receive': 'both'}, + {'afi_safi': 'ipv6', 'send/receive': 'both'}, + {'afi_safi': 'ipv4_lu', 'send/receive': 'receive'}, + {'afi_safi': 'ipv6_lu', 'send/receive': 'receive'}, + {'afi_safi': 'vpnv4', 'send/receive': 'send'}, + {'afi_safi': 'vpnv6', 'send/receive': 'send'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [ + [(1, 1), 3], # ipv4, both + [(2, 1), 3], # ipv6, both + [(1, 4), 1], # ipv4_lu, receive + [(2, 4), 1], # ipv6_lu, receive + [(1, 128), 2], # vpnv4, send + [(2, 128), 2] # vpnv6, send + ] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_evpn(self): + """ + Test converting EVPN + """ + addpath_list = [ + {'afi_safi': 'evpn', 'send/receive': 'both'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(25, 70), 3]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_bgpls(self): + """ + Test converting BGP-LS + """ + addpath_list = [ + {'afi_safi': 'bgpls', 'send/receive': 'receive'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(16388, 71), 1]] + self.assertEqual(expected, result) + + def test_convert_addpath_str_to_int_sr_policy(self): + """ + Test converting SR Policy + """ + addpath_list = [ + {'afi_safi': 'ipv4_srte', 'send/receive': 'both'} + ] + result = convert_addpath_str_to_int(addpath_list) + expected = [[(1, 73), 3]] + self.assertEqual(expected, result) + + +class TestLocalAddPathConfig(unittest.TestCase): + """ + Test cases for local_add_path configuration processing in config.py + Tests the list comprehension that converts local_add_path strings to dictionaries + """ + + def test_local_add_path_single_ipv4_both(self): + """ + Test processing single IPv4 both mode configuration + """ + # Simulate the list comprehension from config.py line 137-139 + local_add_path_config = ['ipv4_both'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4', 'send/receive': 'both'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_single_ipv4_receive(self): + """ + Test processing single IPv4 receive mode configuration + """ + local_add_path_config = ['ipv4_receive'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4', 'send/receive': 'receive'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_single_ipv4_send(self): + """ + Test processing single IPv4 send mode configuration + """ + local_add_path_config = ['ipv4_send'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4', 'send/receive': 'send'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_multiple_families(self): + """ + Test processing multiple address families configuration + """ + local_add_path_config = ['ipv4_both', 'ipv6_receive', 'vpnv4_send'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4', 'send/receive': 'both'}, + {'afi_safi': 'ipv6', 'send/receive': 'receive'}, + {'afi_safi': 'vpnv4', 'send/receive': 'send'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_labeled_unicast(self): + """ + Test processing Labeled Unicast address family configuration + Note: ipv4_lu has two underscores, rsplit('_', 1) only splits the last one + """ + local_add_path_config = ['ipv4_lu_both', 'ipv6_lu_receive'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4_lu', 'send/receive': 'both'}, + {'afi_safi': 'ipv6_lu', 'send/receive': 'receive'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_empty_list(self): + """ + Test processing empty configuration list + """ + local_add_path_config = [] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [] + self.assertEqual(expected, result) + + def test_local_add_path_all_modes(self): + """ + Test all modes (send, receive, both) + """ + local_add_path_config = ['ipv4_send', 'ipv6_receive', 'vpnv4_both'] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4', 'send/receive': 'send'}, + {'afi_safi': 'ipv6', 'send/receive': 'receive'}, + {'afi_safi': 'vpnv4', 'send/receive': 'both'} + ] + self.assertEqual(expected, result) + + def test_local_add_path_complex_afi_safi_names(self): + """ + Test complex address family names (containing multiple underscores) + """ + local_add_path_config = [ + 'ipv4_lu_both', + 'ipv6_lu_receive', + 'ipv4_srte_send', + 'ipv6_flowspec_both' + ] + result = [ + {'afi_safi': item.rsplit('_', 1)[0], 'send/receive': item.rsplit('_', 1)[1]} + for item in local_add_path_config + ] + expected = [ + {'afi_safi': 'ipv4_lu', 'send/receive': 'both'}, + {'afi_safi': 'ipv6_lu', 'send/receive': 'receive'}, + {'afi_safi': 'ipv4_srte', 'send/receive': 'send'}, + {'afi_safi': 'ipv6_flowspec', 'send/receive': 'both'} + ] + self.assertEqual(expected, result) + + +if __name__ == '__main__': + unittest.main() diff --git a/yabgp/tests/unit/message/test_open.py b/yabgp/tests/unit/message/test_open.py index fec3f6a..85585ae 100644 --- a/yabgp/tests/unit/message/test_open.py +++ b/yabgp/tests/unit/message/test_open.py @@ -67,7 +67,7 @@ def test_construct_add_path(self): my_capa = { 'cisco_route_refresh': True, 'route_refresh': True, - 'add_path': 'ipv4_receive', + 'add_path': [{'afi_safi': 'ipv4', 'send/receive': 'receive'}], 'four_bytes_as': True, 'afi_safi': [(1, 1)], 'enhanced_route_refresh': True}