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

Implement the BGP Bounce Back Routing test script #2538

Merged
merged 6 commits into from
Nov 25, 2020
Merged
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
7 changes: 7 additions & 0 deletions tests/bgp/templates/bgp_bbr_config.json.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"BGP_BBR": {
"all": {
"status": "{{ BGP_BBR_STATUS }}"
}
}
}
271 changes: 271 additions & 0 deletions tests/bgp/test_bgp_bbr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
'''This script is to test the BGP Bounce Back Routing (BBR) feature of SONiC.
'''
import json
import logging
import time

from collections import defaultdict
from collections import namedtuple

import pytest
import requests
import ipaddr as ipaddress

from jinja2 import Template
from natsort import natsorted
from tests.common.helpers.assertions import pytest_assert
from tests.common.helpers.parallel import reset_ansible_local_tmp
from tests.common.helpers.parallel import parallel_run

pytestmark = [
pytest.mark.topology('t1'),
pytest.mark.device_type('vs')
]

logger = logging.getLogger(__name__)


EXABGP_BASE_PORT = 5000
EXABGP_BASE_PORT_V6 = 6000


BBR_PREFIX = '172.16.10.0/24'
BBR_PREFIX_V6 = '2000:0:172:16:10::/96'

DUMMY_ASN1 = 64101
DUMMY_ASN2 = 64102


@pytest.fixture(scope='module', autouse=True)
def prepare_bbr_config_files(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]
bgp_bbr_config = Template(open("./bgp/templates/bgp_bbr_config.json.j2").read())

duthost.copy(content=bgp_bbr_config.render(BGP_BBR_STATUS='disabled'), dest='/tmp/disable_bbr.json')
duthost.copy(content=bgp_bbr_config.render(BGP_BBR_STATUS='enabled'), dest='/tmp/enable_bbr.json')


def enable_bbr(duthost):
logger.info('Disable BGP_BBR')
duthost.shell('sonic-cfggen -j /tmp/enable_bbr.json -w ')


def disable_bbr(duthost):
logger.info('Enable BGP_BBR')
duthost.shell('sonic-cfggen -j /tmp/disable_bbr.json -w')


@pytest.fixture
def config_bbr_disabled(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]

disable_bbr(duthost)
time.sleep(3)
yield

enable_bbr(duthost)


@pytest.fixture
def config_bbr_enabled(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]
enable_bbr(duthost)
time.sleep(3)


@pytest.fixture(scope='module')
def setup(duthosts, rand_one_dut_hostname, tbinfo, nbrhosts):
duthost = duthosts[rand_one_dut_hostname]
if tbinfo['topo']['type'] != 't1':
pytest.skip('Unsupported topology type: {}, supported: {}'.format(tbinfo['topo']['type'], 't1'))

mg_facts = duthost.get_extended_minigraph_facts(tbinfo)

tor_neighbors = natsorted([neighbor for neighbor in nbrhosts.keys() if neighbor.endswith('T0')])
t2_neighbors = [neighbor for neighbor in nbrhosts.keys() if neighbor.endswith('T2')]
tor1 = tor_neighbors[0]
other_vms = tor_neighbors[1:] + t2_neighbors

neigh_peer_map = defaultdict(dict)
for bgp_neigh in mg_facts['minigraph_bgp']:
name = bgp_neigh['name']
peer_addr = bgp_neigh['peer_addr']
if ipaddress.IPAddress(peer_addr).version == 4:
neigh_peer_map[name].update({'peer_addr': peer_addr})
else:
neigh_peer_map[name].update({'peer_addr_v6': peer_addr})

# Announce route to one of the T0 VM
tor1_offset = tbinfo['topo']['properties']['topology']['VMs'][tor1]['vm_offset']
tor1_exabgp_port = EXABGP_BASE_PORT + tor1_offset
tor1_exabgp_port_v6 = EXABGP_BASE_PORT_V6 + tor1_offset

dut_asn = tbinfo['topo']['properties']['configuration_properties']['common']['dut_asn']
tor1_asn = nbrhosts[tor1]['conf']['bgp']['asn']
aspath = '{} {}'.format(dut_asn, DUMMY_ASN1)
aspath_dual_dut_asn = '{} {} {} {}'.format(dut_asn, DUMMY_ASN1, dut_asn, DUMMY_ASN2)

Route = namedtuple('Route', ['prefix', 'nexthop', 'aspath'])

bbr_route = Route(BBR_PREFIX, neigh_peer_map[tor1]['peer_addr'], aspath)
bbr_route_v6 = Route(BBR_PREFIX_V6, neigh_peer_map[tor1]['peer_addr_v6'], aspath)

bbr_route_dual_dut_asn = Route(BBR_PREFIX, neigh_peer_map[tor1]['peer_addr'], aspath_dual_dut_asn)
bbr_route_v6_dual_dut_asn = Route(BBR_PREFIX_V6, neigh_peer_map[tor1]['peer_addr_v6'], aspath_dual_dut_asn)

setup_info = {
'tor1': tor1,
'other_vms': other_vms,
'tor1_offset': tor1_offset,
'tor1_exabgp_port': tor1_exabgp_port,
'tor1_exabgp_port_v6': tor1_exabgp_port_v6,
'dut_asn': dut_asn,
'tor1_asn': tor1_asn,
'bbr_route': bbr_route,
'bbr_route_v6': bbr_route_v6,
'bbr_route_dual_dut_asn': bbr_route_dual_dut_asn,
'bbr_route_v6_dual_dut_asn': bbr_route_v6_dual_dut_asn
}

logger.info('setup_info: {}'.format(json.dumps(setup_info, indent=2)))

return setup_info


def update_routes(action, ptfip, port, route):
if action == 'announce':
msg = '{} route {} next-hop {} as-path [ {} ]'.format(action, route.prefix, route.nexthop, route.aspath)
elif action == 'withdraw':
msg = '{} route {} next-hop {}'.format(action, route.prefix, route.nexthop)
else:
logger.error('Unsupported route update operation.')
return
url = 'http://%s:%d' % (ptfip, port)
data = {'commands': msg }
r = requests.post(url, data=data)
assert r.status_code == 200


@pytest.fixture
def prepare_routes(setup, ptfhost):
tor1_exabgp_port = setup['tor1_exabgp_port']
tor1_exabgp_port_v6 = setup['tor1_exabgp_port_v6']

bbr_routes = []

def announce_routes(routes):
logger.info('Announce routes {} to the first T0'.format(str(routes)))
for route in routes:
bbr_routes.append(route)
if ipaddress.IPNetwork(route.prefix).version == 4:
update_routes('announce', ptfhost.mgmt_ip, tor1_exabgp_port, route)
else:
update_routes('announce', ptfhost.mgmt_ip, tor1_exabgp_port_v6, route)
time.sleep(3)

yield announce_routes

logger.info('Withdraw routes {} from the first T0'.format(str(bbr_routes)))
for route in bbr_routes:
if ipaddress.IPNetwork(route.prefix).version == 4:
update_routes('withdraw', ptfhost.mgmt_ip, tor1_exabgp_port, route)
else:
update_routes('withdraw', ptfhost.mgmt_ip, tor1_exabgp_port_v6, route)


def check_bbr_route_propagation(duthost, nbrhosts, setup, route, accepted=True):

tor1 = setup['tor1']
tor1_asn = setup['tor1_asn']
other_vms = setup['other_vms']

# Check route on tor1
logger.info('Check route for prefix {} on {}'.format(route.prefix, tor1))
tor1_route = nbrhosts[tor1]['host'].get_route(route.prefix)
pytest_assert(route.prefix in tor1_route['vrfs']['default']['bgpRouteEntries'].keys(),
'No route for {} found on {}'.format(route.prefix, tor1))
tor1_route_aspath = tor1_route['vrfs']['default']['bgpRouteEntries'][route.prefix]['bgpRoutePaths'][0]\
['asPathEntry']['asPath']
pytest_assert(tor1_route_aspath==route.aspath,
'On {} expected aspath: {}, actual aspath: {}'.format(tor1, route.aspath, tor1_route_aspath))

# Check route on DUT
logger.info('Check route on DUT')
dut_route = duthost.get_route(route.prefix)
if accepted:
pytest_assert(dut_route, 'No route for {} found on DUT'.format(route.prefix))
dut_route_aspath = dut_route['paths'][0]['aspath']['string']
# Route path from DUT: -> TOR1 -> aspath(other T1 -> DUMMY_ASN1)
dut_route_aspath_expected = '{} {}'.format(tor1_asn, route.aspath)
pytest_assert(dut_route_aspath==dut_route_aspath_expected,
'On DUT expected aspath: {}, actual aspath: {}'.format(dut_route_aspath_expected, dut_route_aspath))
else:
pytest_assert(not dut_route, 'Prefix {} should not be accepted by DUT'.format(route.prefix))

# Check route on other VMs
@reset_ansible_local_tmp
def check_other_vms(nbrhosts, setup, route, accepted=True, node=None, results=None):
logger.info('Check route {} on {}'.format(str(route), node))

dut_asn = setup['dut_asn']
tor1_asn = setup['tor1_asn']

vm_route = nbrhosts[node]['host'].get_route(route.prefix)
vm_route = {'failed': False}
vm_route['tor_route'] = vm_route
if accepted:
if route.prefix not in vm_route['vrfs']['default']['bgpRouteEntries'].keys():
vm_route['failed'] = True
vm_route['message'] = 'No route for {} found on {}'.format(route.prefix, node)
else:
tor_route_aspath = vm_route['vrfs']['default']['bgpRouteEntries'][route.prefix]['bgpRoutePaths'][0]\
['asPathEntry']['asPath']
# Route path from other VMs: -> DUT(T1) -> TOR1 -> aspath(other T1 -> DUMMY_ASN1)
tor_route_aspath_expected = '{} {} {}'.format(dut_asn, tor1_asn, route.aspath)
if tor_route_aspath != tor_route_aspath_expected:
vm_route['failed'] = True
vm_route['message'] = 'On {} expected aspath: {}, actual aspath: {}'\
.format(node, tor_route_aspath_expected, tor_route_aspath)
else:
if route.prefix in vm_route['vrfs']['default']['bgpRouteEntries'].keys():
vm_route['failed'] = True
vm_route['message'] = 'No route {} expected on {}'.format(route.prefix, node)
vm_route['message'] = 'Checking route {} on {} passed'.format(str(route), node)
results[node] = vm_route

results = parallel_run(check_other_vms, (nbrhosts, setup, route), {'accepted': accepted}, other_vms, timeout=120)

failed_results = {}
for node, result in results.items():
if result['failed']:
failed_results[node] = result

pytest_assert(not failed_results, 'Checking route {} failed, failed_results: {}'\
.format(str(route), json.dumps(failed_results, indent=2)))


def test_bbr_enabled_dut_asn_in_aspath(duthosts, rand_one_dut_hostname, nbrhosts, config_bbr_enabled, setup, prepare_routes):
duthost = duthosts[rand_one_dut_hostname]
bbr_route = setup['bbr_route']
bbr_route_v6 = setup['bbr_route_v6']
prepare_routes([bbr_route, bbr_route_v6])
for route in [bbr_route, bbr_route_v6]:
check_bbr_route_propagation(duthost, nbrhosts, setup, route, accepted=True)


def test_bbr_enabled_dual_dut_asn_in_aspath(duthosts, rand_one_dut_hostname, nbrhosts, config_bbr_enabled, setup, prepare_routes):
duthost = duthosts[rand_one_dut_hostname]
bbr_route_dual_dut_asn = setup['bbr_route_dual_dut_asn']
bbr_route_v6_dual_dut_asn = setup['bbr_route_v6_dual_dut_asn']
prepare_routes([bbr_route_dual_dut_asn, bbr_route_v6_dual_dut_asn])
for route in [bbr_route_dual_dut_asn, bbr_route_v6_dual_dut_asn]:
check_bbr_route_propagation(duthost, nbrhosts, setup, route, accepted=False)


def test_bbr_disabled_dut_asn_in_aspath(duthosts, rand_one_dut_hostname, nbrhosts, config_bbr_disabled, setup, prepare_routes):
duthost = duthosts[rand_one_dut_hostname]
bbr_route = setup['bbr_route']
bbr_route_v6 = setup['bbr_route_v6']
prepare_routes([bbr_route, bbr_route_v6])
for route in (bbr_route, bbr_route_v6):
check_bbr_route_propagation(duthost, nbrhosts, setup, route, accepted=False)
11 changes: 11 additions & 0 deletions tests/common/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,10 @@ def get_extended_minigraph_facts(self, tbinfo):

return mg_facts

def get_route(self, prefix):
cmd = 'show bgp ipv4' if ipaddress.ip_network(unicode(prefix)).version == 4 else 'show bgp ipv6'
return json.loads(self.shell('vtysh -c "{} {} json"'.format(cmd, prefix))['stdout'])


class K8sMasterHost(AnsibleHostBase):
"""
Expand Down Expand Up @@ -1387,6 +1391,13 @@ def exec_template(self, ansible_root, ansible_playbook, inventory, **kwargs):
if res["localhost"]["rc"] != 0:
raise Exception("Unable to execute template\n{}".format(res["stdout"]))

def get_route(self, prefix):
cmd = 'show ip bgp' if ipaddress.ip_network(unicode(prefix)).version == 4 else 'show ipv6 bgp'
return self.eos_command(commands=[{
'command': '{} {}'.format(cmd, prefix),
'output': 'json'
}])['stdout'][0]


class OnyxHost(AnsibleHostBase):
"""
Expand Down