Skip to content

Commit 0d5161b

Browse files
committed
Add case 'test_bgp_update_timer'
Add new testcase `test_bgp_update_timer` to ensure BGP updates are propagated within certain time threshold. For FRRouting, time interval for the update in and update out for the same route must be within 1s. For Quagga, the time interval must be within 20s. Signed-off-by: Longxiang Lyu <lolv@microsoft.com>
1 parent f97fe26 commit 0d5161b

2 files changed

Lines changed: 361 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"DEVICE_NEIGHBOR_METADATA": {
3+
"{{ neighbor_name }}": {
4+
"lo_addr": "{{ neighbor_lo_addr }}",
5+
"mgmt_addr": "{{ neighbor_mgmt_addr }}",
6+
"hwsku": "{{ neighbor_hwsku }}",
7+
"type": "{{ neighbor_type }}"
8+
}
9+
}
10+
}

tests/bgp/test_bgp_update_timer.py

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
"""Check how fast FRR or QUAGGA will send updates to neighbors."""
2+
import contextlib
3+
import ipaddress
4+
import logging
5+
import pytest
6+
import requests
7+
import tempfile
8+
import time
9+
10+
from scapy.all import sniff, IP
11+
from scapy.contrib import bgp
12+
from tests.common.utilities import wait_tcp_connection
13+
14+
15+
pytestmark = [
16+
pytest.mark.topology("any"),
17+
]
18+
19+
BGP_SAVE_DEST_TMPL = "/tmp/bgp_%s.j2"
20+
NEIGHBOR_SAVE_DEST_TMPL = "/tmp/neighbor_%s.j2"
21+
BGP_LOG_TMPL = "/tmp/bgp%d.pcap"
22+
ANNOUNCED_SUBNETS = [
23+
"10.10.100.0/27",
24+
"10.10.100.32/27",
25+
"10.10.100.64/27",
26+
"10.10.100.96/27",
27+
"10.10.100.128/27"
28+
]
29+
NEIGHBOR_ASN0 = 61000
30+
NEIGHBOR_ASN1 = 61001
31+
NEIGHBOR_PORT0 = 11000
32+
NEIGHBOR_PORT1 = 11001
33+
34+
35+
def _write_variable_from_j2_to_configdb(duthost, template_file, **kwargs):
36+
save_dest_path = kwargs.pop("save_dest_path", "/tmp/temp.j2")
37+
keep_dest_file = kwargs.pop("keep_dest_file", False)
38+
duthost.host.options["variable_manager"].extra_vars.update(kwargs)
39+
duthost.template(src=template_file, dest=save_dest_path)
40+
duthost.shell("sonic-cfggen -j %s --write-to-db" % save_dest_path)
41+
if not keep_dest_file:
42+
duthost.file(path=save_dest_path, state="absent")
43+
44+
45+
class BGPNeighbor(object):
46+
47+
def __init__(self, duthost, ptfhost, name, iface,
48+
neighbor_ip, neighbor_asn,
49+
dut_ip, dut_asn, port, is_quagga=False):
50+
self.duthost = duthost
51+
self.ptfhost = ptfhost
52+
self.ptfip = ptfhost.host.options["inventory_manager"].get_host(
53+
ptfhost.hostname).vars["ansible_host"]
54+
self.iface = iface
55+
self.name = name
56+
self.ip = neighbor_ip
57+
self.asn = neighbor_asn
58+
self.peer_ip = dut_ip
59+
self.peer_asn = dut_asn
60+
self.port = port
61+
self.is_quagga = is_quagga
62+
63+
def start_session(self):
64+
"""Start the BGP session."""
65+
logging.debug("start bgp session %s", self.name)
66+
self.ptfhost.shell("ifconfig %s %s/32" % (self.iface, self.ip))
67+
self.ptfhost.exabgp(
68+
name=self.name,
69+
state="started",
70+
local_ip=self.ip,
71+
router_id=self.ip,
72+
peer_ip=self.peer_ip,
73+
local_asn=self.asn,
74+
peer_asn=self.peer_asn,
75+
port=self.port
76+
)
77+
if not wait_tcp_connection(self.ptfhost, self.ptfip, self.port):
78+
raise RuntimeError("Failed to start BGP neighbor %s" % self.name)
79+
80+
_write_variable_from_j2_to_configdb(
81+
self.duthost,
82+
"bgp/templates/neighbor_metadata_template.j2",
83+
save_dest_path=NEIGHBOR_SAVE_DEST_TMPL % self.name,
84+
neighbor_name=self.name,
85+
neighbor_lo_addr=self.ip,
86+
neighbor_mgmt_addr=self.ip,
87+
neighbor_hwsku=None,
88+
neighbor_type="ToRRouter"
89+
)
90+
91+
_write_variable_from_j2_to_configdb(
92+
self.duthost,
93+
"bgp/templates/bgp_template.j2",
94+
save_dest_path=BGP_SAVE_DEST_TMPL % self.name,
95+
db_table_name="BGP_NEIGHBOR",
96+
peer_addr=self.ip,
97+
asn=self.asn,
98+
local_addr=self.peer_ip,
99+
peer_name=self.name
100+
)
101+
102+
if self.is_quagga:
103+
allow_ebgp_multihop_cmd = (
104+
"vtysh "
105+
"-c 'configure terminal' "
106+
"-c 'router bgp %s' "
107+
"-c 'neighbor %s ebgp-multihop'"
108+
)
109+
allow_ebgp_multihop_cmd %= (self.peer_asn, self.ip)
110+
self.duthost.shell(allow_ebgp_multihop_cmd)
111+
112+
# populate DUT arp table
113+
self.duthost.shell("ping -c 3 %s" % (self.ip))
114+
115+
def stop_session(self):
116+
"""Stop the BGP session."""
117+
logging.debug("stop bgp session %s", self.name)
118+
self.duthost.shell("redis-cli -n 4 -c DEL 'BGP_NEIGHBOR|%s'" % self.ip)
119+
self.duthost.shell("redis-cli -n 4 -c DEL 'DEVICE_NEIGHBOR_METADATA|%s'" % self.name)
120+
self.ptfhost.exabgp(name=self.name, state="absent")
121+
self.ptfhost.shell("ifconfig %s 0.0.0.0" % self.iface)
122+
123+
# TODO: let's put those BGP utilities function in a common place.
124+
def announce_route(self, route):
125+
if "aspath" in route:
126+
msg = "announce route {prefix} next-hop {nexthop} as-path [ {aspath} ]"
127+
else:
128+
msg = "announce route {prefix} next-hop {nexthop}"
129+
msg = msg.format(**route)
130+
logging.debug("announce route: %s", msg)
131+
url = "http://%s:%d" % (self.ptfip, self.port)
132+
resp = requests.post(url, data={"commands": msg})
133+
logging.debug("announce return: %s", resp)
134+
assert resp.status_code == 200
135+
136+
def withdraw_route(self, route):
137+
if "aspath" in route:
138+
msg = "withdraw route {prefix} next-hop {nexthop} as-path [ {aspath} ]"
139+
else:
140+
msg = "withdraw route {prefix} next-hop {nexthop}"
141+
msg = msg.format(**route)
142+
logging.debug("withdraw route: %s", msg)
143+
url = "http://%s:%d" % (self.ptfip, self.port)
144+
resp = requests.post(url, data={"commands": msg})
145+
logging.debug("withdraw return: %s", resp)
146+
assert resp.status_code == 200
147+
148+
149+
@contextlib.contextmanager
150+
def log_bgp_updates(duthost, iface, save_path):
151+
"""Capture bgp packets to file."""
152+
start_pcap = "tcpdump -i %s -w %s port 179" % (iface, save_path)
153+
stop_pcap = "pkill -f '%s'" % start_pcap
154+
start_pcap = "nohup %s &" % start_pcap
155+
duthost.shell(start_pcap)
156+
try:
157+
yield
158+
finally:
159+
duthost.shell(stop_pcap, module_ignore_errors=True)
160+
161+
162+
@pytest.fixture
163+
def is_quagga(duthost):
164+
"""Return True if current bgp is using Quagga."""
165+
show_res = duthost.shell("vtysh -c 'show version'")
166+
return "Quagga" in show_res["stdout"]
167+
168+
169+
@pytest.fixture
170+
def common_setup_teardown(duthost, is_quagga, ptfhost):
171+
mg_facts = duthost.minigraph_facts(host=duthost.hostname)["ansible_facts"]
172+
173+
dut_asn = mg_facts["minigraph_bgp_asn"]
174+
dut_lo_addr = mg_facts["minigraph_lo_interfaces"][0]["addr"]
175+
dut_mgmt_iface = mg_facts["minigraph_mgmt_interface"]["alias"]
176+
dut_mgmt_addr = mg_facts["minigraph_mgmt_interface"]["addr"]
177+
bgp_neighbors = (
178+
BGPNeighbor(
179+
duthost,
180+
ptfhost,
181+
"pseudoswitch0",
182+
"mgmt:0",
183+
"10.10.10.10",
184+
NEIGHBOR_ASN0,
185+
dut_lo_addr,
186+
dut_asn,
187+
NEIGHBOR_PORT0,
188+
is_quagga=is_quagga
189+
),
190+
BGPNeighbor(
191+
duthost,
192+
ptfhost,
193+
"pseudoswitch1",
194+
"mgmt:1",
195+
"10.10.10.11",
196+
NEIGHBOR_ASN1,
197+
dut_lo_addr,
198+
dut_asn,
199+
NEIGHBOR_PORT1,
200+
is_quagga=is_quagga
201+
)
202+
)
203+
204+
add_route_tmpl = "ip route add %s/32 via %s dev %s"
205+
ptfhost.shell(add_route_tmpl % (dut_lo_addr, dut_mgmt_addr, "mgmt"))
206+
duthost.shell(add_route_tmpl % (bgp_neighbors[0].ip, bgp_neighbors[0].ptfip, dut_mgmt_iface))
207+
duthost.shell(add_route_tmpl % (bgp_neighbors[1].ip, bgp_neighbors[0].ptfip, dut_mgmt_iface))
208+
209+
yield bgp_neighbors, dut_mgmt_iface
210+
211+
flush_route_tmpl = "ip route flush %s/32"
212+
ptfhost.shell(flush_route_tmpl % dut_lo_addr)
213+
duthost.shell(flush_route_tmpl % bgp_neighbors[0].ip)
214+
duthost.shell(flush_route_tmpl % bgp_neighbors[1].ip)
215+
duthost.shell("sonic-clear arp")
216+
217+
218+
@pytest.fixture
219+
def constants(is_quagga, ptfhost):
220+
class _C(object):
221+
"""Dummy class to save test constants."""
222+
pass
223+
224+
_constants = _C()
225+
if is_quagga:
226+
_constants.sleep_interval = 40
227+
_constants.update_interval_threshold = 20
228+
else:
229+
_constants.sleep_interval = 5
230+
_constants.update_interval_threshold = 1
231+
232+
_constants.routes = []
233+
ptfip = ptfhost.host.options["inventory_manager"].get_host(ptfhost.hostname).vars["ansible_host"]
234+
for subnet in ANNOUNCED_SUBNETS:
235+
_constants.routes.append(
236+
{"prefix": subnet, "nexthop": ptfip}
237+
)
238+
return _constants
239+
240+
241+
def test_bgp_update_timer(common_setup_teardown, constants, duthost):
242+
243+
def bgp_update_packets(pcap_file):
244+
"""Get bgp update packets from pcap file."""
245+
packets = sniff(
246+
offline=pcap_file,
247+
lfilter=lambda p: bgp.BGPHeader in p and p[bgp.BGPHeader].type == 2
248+
)
249+
return packets
250+
251+
def match_bgp_update(packet, src_ip, dst_ip, action, route):
252+
"""Check if the bgp update packet matches."""
253+
if not (packet[IP].src == src_ip and packet[IP].dst == dst_ip):
254+
return False
255+
subnet = ipaddress.ip_network(route["prefix"].decode())
256+
_route = (subnet.prefixlen, str(subnet.network_address))
257+
bgp_fields = packet[bgp.BGPUpdate].fields
258+
if action == "announce":
259+
return "nlri" in bgp_fields and _route in bgp_fields["nlri"]
260+
elif action == "withdraw":
261+
return _route in bgp_fields["withdrawn"]
262+
else:
263+
return False
264+
265+
(n0, n1), dut_mgmt_iface = common_setup_teardown
266+
try:
267+
n0.start_session()
268+
n1.start_session()
269+
270+
# sleep till new sessions are steady
271+
time.sleep(30)
272+
273+
# ensure new sessions are ready
274+
bgp_facts = duthost.bgp_facts()["ansible_facts"]
275+
assert n0.ip in bgp_facts["bgp_neighbors"]
276+
assert n1.ip in bgp_facts["bgp_neighbors"]
277+
assert bgp_facts["bgp_neighbors"][n0.ip]["state"] == "established"
278+
assert bgp_facts["bgp_neighbors"][n1.ip]["state"] == "established"
279+
280+
announce_intervals = []
281+
withdraw_intervals = []
282+
for i, route in enumerate(constants.routes):
283+
bgp_pcap = BGP_LOG_TMPL % i
284+
with log_bgp_updates(duthost, dut_mgmt_iface, bgp_pcap):
285+
n0.announce_route(route)
286+
time.sleep(constants.sleep_interval)
287+
n0.withdraw_route(route)
288+
time.sleep(constants.sleep_interval)
289+
290+
with tempfile.NamedTemporaryFile() as tmp_pcap:
291+
duthost.fetch(src=bgp_pcap, dest=tmp_pcap.name, flat=True)
292+
bgp_updates = bgp_update_packets(tmp_pcap.name)
293+
294+
announce_from_n0_to_dut = []
295+
announce_from_dut_to_n1 = []
296+
withdraw_from_n0_to_dut = []
297+
withdraw_from_dut_to_n1 = []
298+
for bgp_update in bgp_updates:
299+
if match_bgp_update(bgp_update, n0.ip, n0.peer_ip, "announce", route):
300+
announce_from_n0_to_dut.append(bgp_update)
301+
continue
302+
if match_bgp_update(bgp_update, n1.peer_ip, n1.ip, "announce", route):
303+
announce_from_dut_to_n1.append(bgp_update)
304+
continue
305+
if match_bgp_update(bgp_update, n0.ip, n0.peer_ip, "withdraw", route):
306+
withdraw_from_n0_to_dut.append(bgp_update)
307+
continue
308+
if match_bgp_update(bgp_update, n1.peer_ip, n1.ip, "withdraw", route):
309+
withdraw_from_dut_to_n1.append(bgp_update)
310+
311+
err_msg = "no bgp update %s route %s from %s to %s"
312+
no_update = False
313+
if not announce_from_n0_to_dut:
314+
err_msg %= ("announce", route, n0.ip, n0.peer_ip)
315+
no_update = True
316+
elif not announce_from_dut_to_n1:
317+
err_msg %= ("announce", route, n1.peer_ip, n1.ip)
318+
no_update = True
319+
elif not withdraw_from_n0_to_dut:
320+
err_msg %= ("withdraw", route, n0.ip, n0.peer_ip)
321+
no_update = True
322+
elif not withdraw_from_dut_to_n1:
323+
err_msg %= ("withdraw", route, n1.peer_ip, n1.ip)
324+
no_update = True
325+
if no_update:
326+
pytest.fail(err_msg)
327+
328+
announce_intervals.append(
329+
announce_from_dut_to_n1[0].time - announce_from_n0_to_dut[0].time
330+
)
331+
withdraw_intervals.append(
332+
withdraw_from_dut_to_n1[0].time - withdraw_from_n0_to_dut[0].time
333+
)
334+
335+
logging.debug("announce updates intervals: %s", announce_intervals)
336+
logging.debug("withdraw updates intervals: %s", withdraw_intervals)
337+
338+
mi = (len(constants.routes) - 1) // 2
339+
announce_intervals.sort()
340+
withdraw_intervals.sort()
341+
err_msg = "%s updates interval exceeds threshold %d"
342+
if announce_intervals[mi] >= constants.update_interval_threshold:
343+
pytest.fail(err_msg % ("announce", constants.update_interval_threshold))
344+
if withdraw_intervals[mi] >= constants.update_interval_threshold:
345+
pytest.fail(err_msg % ("withdraw", constants.update_interval_threshold))
346+
347+
finally:
348+
n0.stop_session()
349+
n1.stop_session()
350+
for route in constants.routes:
351+
duthost.shell("ip route flush %s" % route["prefix"])

0 commit comments

Comments
 (0)