Skip to content
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
2 changes: 1 addition & 1 deletion ansible/library/exabgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
try:
line = stdin.readline()
obj = json.loads(line)
f = open("/tmp/exabgp-" + obj["neighbor"]["ip"], "a")
f = open("/tmp/exabgp-" + obj["neighbor"]["address"]["local"], "a")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because DUT uses loopback address as router ID for both sessions, those two exabgp will dump to the same file.

print >> f, line,
f.close()
except:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"BGP_MONITORS": {
"{{ db_table_name }}": {
"{{ peer_addr }}": {
"admin_status": "up",
"asn": "{{ asn }}",
Expand Down
10 changes: 10 additions & 0 deletions tests/bgp/templates/neighbor_metadata_template.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"DEVICE_NEIGHBOR_METADATA": {
"{{ neighbor_name }}": {
"lo_addr": "{{ neighbor_lo_addr }}",
"mgmt_addr": "{{ neighbor_mgmt_addr }}",
"hwsku": "{{ neighbor_hwsku }}",
"type": "{{ neighbor_type }}"
}
}
}
350 changes: 350 additions & 0 deletions tests/bgp/test_bgp_update_timer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
"""Check how fast FRR or QUAGGA will send updates to neighbors."""
import contextlib
import ipaddress
import jinja2
import logging
import pytest
import requests
import tempfile
import time

from scapy.all import sniff, IP
from scapy.contrib import bgp
from tests.common.utilities import wait_tcp_connection


pytestmark = [
pytest.mark.topology("any"),
]

BGP_SAVE_DEST_TMPL = "/tmp/bgp_%s.j2"
NEIGHBOR_SAVE_DEST_TMPL = "/tmp/neighbor_%s.j2"
BGP_LOG_TMPL = "/tmp/bgp%d.pcap"
ANNOUNCED_SUBNETS = [
"10.10.100.0/27",
"10.10.100.32/27",
"10.10.100.64/27",
"10.10.100.96/27",
"10.10.100.128/27"
]
NEIGHBOR_ASN0 = 61000
NEIGHBOR_ASN1 = 61001
NEIGHBOR_PORT0 = 11000
NEIGHBOR_PORT1 = 11001


def _write_variable_from_j2_to_configdb(duthost, template_file, **kwargs):
save_dest_path = kwargs.pop("save_dest_path", "/tmp/temp.j2")
keep_dest_file = kwargs.pop("keep_dest_file", False)
config_template = jinja2.Template(open(template_file).read())
duthost.copy(content=config_template.render(**kwargs), dest=save_dest_path)
duthost.shell("sonic-cfggen -j %s --write-to-db" % save_dest_path)
if not keep_dest_file:
duthost.file(path=save_dest_path, state="absent")


class BGPNeighbor(object):

def __init__(self, duthost, ptfhost, name, iface,
neighbor_ip, neighbor_asn,
dut_ip, dut_asn, port, is_quagga=False):
self.duthost = duthost
self.ptfhost = ptfhost
self.ptfip = ptfhost.mgmt_ip
self.iface = iface
self.name = name
self.ip = neighbor_ip
self.asn = neighbor_asn
self.peer_ip = dut_ip
self.peer_asn = dut_asn
self.port = port
self.is_quagga = is_quagga

def start_session(self):
"""Start the BGP session."""
logging.debug("start bgp session %s", self.name)
self.ptfhost.shell("ifconfig %s %s/32" % (self.iface, self.ip))
self.ptfhost.exabgp(
name=self.name,
state="started",
local_ip=self.ip,
router_id=self.ip,
peer_ip=self.peer_ip,
local_asn=self.asn,
peer_asn=self.peer_asn,
port=self.port
)
if not wait_tcp_connection(self.ptfhost, self.ptfip, self.port):
raise RuntimeError("Failed to start BGP neighbor %s" % self.name)

_write_variable_from_j2_to_configdb(
self.duthost,
"bgp/templates/neighbor_metadata_template.j2",
save_dest_path=NEIGHBOR_SAVE_DEST_TMPL % self.name,
neighbor_name=self.name,
neighbor_lo_addr=self.ip,
neighbor_mgmt_addr=self.ip,
neighbor_hwsku=None,
neighbor_type="ToRRouter"
)

_write_variable_from_j2_to_configdb(
self.duthost,
"bgp/templates/bgp_template.j2",
save_dest_path=BGP_SAVE_DEST_TMPL % self.name,
db_table_name="BGP_NEIGHBOR",
peer_addr=self.ip,
asn=self.asn,
local_addr=self.peer_ip,
peer_name=self.name
)

if self.is_quagga:
allow_ebgp_multihop_cmd = (
"vtysh "
"-c 'configure terminal' "
"-c 'router bgp %s' "
"-c 'neighbor %s ebgp-multihop'"
)
allow_ebgp_multihop_cmd %= (self.peer_asn, self.ip)
self.duthost.shell(allow_ebgp_multihop_cmd)

# populate DUT arp table
self.duthost.shell("ping -c 3 %s" % (self.ip))

def stop_session(self):
"""Stop the BGP session."""
logging.debug("stop bgp session %s", self.name)
self.duthost.shell("redis-cli -n 4 -c DEL 'BGP_NEIGHBOR|%s'" % self.ip)
self.duthost.shell("redis-cli -n 4 -c DEL 'DEVICE_NEIGHBOR_METADATA|%s'" % self.name)
self.ptfhost.exabgp(name=self.name, state="absent")
self.ptfhost.shell("ifconfig %s 0.0.0.0" % self.iface)

# TODO: let's put those BGP utility functions in a common place.
def announce_route(self, route):
if "aspath" in route:
msg = "announce route {prefix} next-hop {nexthop} as-path [ {aspath} ]"
else:
msg = "announce route {prefix} next-hop {nexthop}"
msg = msg.format(**route)
logging.debug("announce route: %s", msg)
url = "http://%s:%d" % (self.ptfip, self.port)
resp = requests.post(url, data={"commands": msg})
logging.debug("announce return: %s", resp)
assert resp.status_code == 200

def withdraw_route(self, route):
if "aspath" in route:
msg = "withdraw route {prefix} next-hop {nexthop} as-path [ {aspath} ]"
else:
msg = "withdraw route {prefix} next-hop {nexthop}"
msg = msg.format(**route)
logging.debug("withdraw route: %s", msg)
url = "http://%s:%d" % (self.ptfip, self.port)
resp = requests.post(url, data={"commands": msg})
logging.debug("withdraw return: %s", resp)
assert resp.status_code == 200


@contextlib.contextmanager
def log_bgp_updates(duthost, iface, save_path):
"""Capture bgp packets to file."""
start_pcap = "tcpdump -i %s -w %s port 179" % (iface, save_path)
stop_pcap = "pkill -f '%s'" % start_pcap
start_pcap = "nohup %s &" % start_pcap
duthost.shell(start_pcap)
try:
yield
finally:
duthost.shell(stop_pcap, module_ignore_errors=True)


@pytest.fixture
def is_quagga(duthost):
"""Return True if current bgp is using Quagga."""
show_res = duthost.shell("vtysh -c 'show version'")
return "Quagga" in show_res["stdout"]


@pytest.fixture
def common_setup_teardown(duthost, is_quagga, ptfhost):
mg_facts = duthost.minigraph_facts(host=duthost.hostname)["ansible_facts"]

dut_asn = mg_facts["minigraph_bgp_asn"]
dut_lo_addr = mg_facts["minigraph_lo_interfaces"][0]["addr"]
dut_mgmt_iface = mg_facts["minigraph_mgmt_interface"]["alias"]
dut_mgmt_addr = mg_facts["minigraph_mgmt_interface"]["addr"]
bgp_neighbors = (
BGPNeighbor(
duthost,
ptfhost,
"pseudoswitch0",
"mgmt:0",
"10.10.10.10",
NEIGHBOR_ASN0,
dut_lo_addr,
dut_asn,
NEIGHBOR_PORT0,
is_quagga=is_quagga
),
BGPNeighbor(
duthost,
ptfhost,
"pseudoswitch1",
"mgmt:1",
"10.10.10.11",
NEIGHBOR_ASN1,
dut_lo_addr,
dut_asn,
NEIGHBOR_PORT1,
is_quagga=is_quagga
)
)

add_route_tmpl = "ip route add %s/32 via %s dev %s"
ptfhost.shell(add_route_tmpl % (dut_lo_addr, dut_mgmt_addr, "mgmt"))
duthost.shell(add_route_tmpl % (bgp_neighbors[0].ip, bgp_neighbors[0].ptfip, dut_mgmt_iface))
duthost.shell(add_route_tmpl % (bgp_neighbors[1].ip, bgp_neighbors[0].ptfip, dut_mgmt_iface))

yield bgp_neighbors, dut_mgmt_iface

flush_route_tmpl = "ip route flush %s/32"
ptfhost.shell(flush_route_tmpl % dut_lo_addr)
duthost.shell(flush_route_tmpl % bgp_neighbors[0].ip)
duthost.shell(flush_route_tmpl % bgp_neighbors[1].ip)
duthost.shell("sonic-clear arp")


@pytest.fixture
def constants(is_quagga, ptfhost):
class _C(object):
"""Dummy class to save test constants."""
pass

_constants = _C()
if is_quagga:
_constants.sleep_interval = 40
_constants.update_interval_threshold = 20
else:
_constants.sleep_interval = 5
_constants.update_interval_threshold = 1

_constants.routes = []
for subnet in ANNOUNCED_SUBNETS:
_constants.routes.append(
{"prefix": subnet, "nexthop": ptfhost.mgmt_ip}
)
return _constants


def test_bgp_update_timer(common_setup_teardown, constants, duthost):

def bgp_update_packets(pcap_file):
"""Get bgp update packets from pcap file."""
packets = sniff(
offline=pcap_file,
lfilter=lambda p: bgp.BGPHeader in p and p[bgp.BGPHeader].type == 2
)
return packets

def match_bgp_update(packet, src_ip, dst_ip, action, route):
"""Check if the bgp update packet matches."""
if not (packet[IP].src == src_ip and packet[IP].dst == dst_ip):
return False
subnet = ipaddress.ip_network(route["prefix"].decode())
_route = (subnet.prefixlen, str(subnet.network_address))
bgp_fields = packet[bgp.BGPUpdate].fields
if action == "announce":
return bgp_fields["tp_len"] > 0 and _route in bgp_fields["nlri"]
elif action == "withdraw":
return bgp_fields["withdrawn_len"] > 0 and _route in bgp_fields["withdrawn"]
else:
return False

(n0, n1), dut_mgmt_iface = common_setup_teardown
try:
n0.start_session()
n1.start_session()

# sleep till new sessions are steady
time.sleep(30)

# ensure new sessions are ready
bgp_facts = duthost.bgp_facts()["ansible_facts"]
assert n0.ip in bgp_facts["bgp_neighbors"]
assert n1.ip in bgp_facts["bgp_neighbors"]
assert bgp_facts["bgp_neighbors"][n0.ip]["state"] == "established"
assert bgp_facts["bgp_neighbors"][n1.ip]["state"] == "established"

announce_intervals = []
withdraw_intervals = []
for i, route in enumerate(constants.routes):
bgp_pcap = BGP_LOG_TMPL % i
with log_bgp_updates(duthost, dut_mgmt_iface, bgp_pcap):
n0.announce_route(route)
time.sleep(constants.sleep_interval)
n0.withdraw_route(route)
time.sleep(constants.sleep_interval)

with tempfile.NamedTemporaryFile() as tmp_pcap:
duthost.fetch(src=bgp_pcap, dest=tmp_pcap.name, flat=True)
bgp_updates = bgp_update_packets(tmp_pcap.name)

announce_from_n0_to_dut = []
announce_from_dut_to_n1 = []
withdraw_from_n0_to_dut = []
withdraw_from_dut_to_n1 = []
for bgp_update in bgp_updates:
if match_bgp_update(bgp_update, n0.ip, n0.peer_ip, "announce", route):
announce_from_n0_to_dut.append(bgp_update)
continue
if match_bgp_update(bgp_update, n1.peer_ip, n1.ip, "announce", route):
announce_from_dut_to_n1.append(bgp_update)
continue
if match_bgp_update(bgp_update, n0.ip, n0.peer_ip, "withdraw", route):
withdraw_from_n0_to_dut.append(bgp_update)
continue
if match_bgp_update(bgp_update, n1.peer_ip, n1.ip, "withdraw", route):
withdraw_from_dut_to_n1.append(bgp_update)

err_msg = "no bgp update %s route %s from %s to %s"
no_update = False
if not announce_from_n0_to_dut:
err_msg %= ("announce", route, n0.ip, n0.peer_ip)
no_update = True
elif not announce_from_dut_to_n1:
err_msg %= ("announce", route, n1.peer_ip, n1.ip)
no_update = True
elif not withdraw_from_n0_to_dut:
err_msg %= ("withdraw", route, n0.ip, n0.peer_ip)
no_update = True
elif not withdraw_from_dut_to_n1:
err_msg %= ("withdraw", route, n1.peer_ip, n1.ip)
no_update = True
if no_update:
pytest.fail(err_msg)

announce_intervals.append(
announce_from_dut_to_n1[0].time - announce_from_n0_to_dut[0].time
)
withdraw_intervals.append(
withdraw_from_dut_to_n1[0].time - withdraw_from_n0_to_dut[0].time
)

logging.debug("announce updates intervals: %s", announce_intervals)
logging.debug("withdraw updates intervals: %s", withdraw_intervals)

mi = (len(constants.routes) - 1) // 2
announce_intervals.sort()
withdraw_intervals.sort()
err_msg = "%s updates interval exceeds threshold %d"
if announce_intervals[mi] >= constants.update_interval_threshold:
pytest.fail(err_msg % ("announce", constants.update_interval_threshold))
if withdraw_intervals[mi] >= constants.update_interval_threshold:
pytest.fail(err_msg % ("withdraw", constants.update_interval_threshold))

finally:
n0.stop_session()
n1.stop_session()
for route in constants.routes:
duthost.shell("ip route flush %s" % route["prefix"])
3 changes: 2 additions & 1 deletion tests/bgp/test_bgpmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
pytest.mark.topology('any'),
]

BGPMON_TEMPLATE_FILE = 'bgp/templates/bgpmon.j2'
BGPMON_TEMPLATE_FILE = 'bgp/templates/bgp_template.j2'
BGPMON_CONFIG_FILE = '/tmp/bgpmon.json'
BGP_PORT = 179
BGP_CONNECT_TIMEOUT = 121
Expand Down Expand Up @@ -70,6 +70,7 @@ def common_setup_teardown(duthost, ptfhost):
# Assign peer addr to an interface on ptf
logger.info("Generated peer address {}".format(peer_addr))
bgpmon_args = {
'db_table_name': 'BGP_MONITORS',
'peer_addr': peer_addr,
'asn': mg_facts['minigraph_bgp_asn'],
'local_addr': local_addr,
Expand Down
Loading