-
Notifications
You must be signed in to change notification settings - Fork 1k
[pytest] Add new test to check BGP update time #2594
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e6d53cc
[bgpmon.j2] Add variable 'db_table_name'
lolyu f97fe26
[exabgp] Use neighbor ip to name BGP dump file
lolyu 0d5161b
Add case 'test_bgp_update_timer'
lolyu ef1f236
Fix comments
lolyu c443607
Change BGP update match conditions
lolyu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
tests/bgp/templates/bgpmon.j2 → tests/bgp/templates/bgp_template.j2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
lolyu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # 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"]) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why make this change?
There was a problem hiding this comment.
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
exabgpwill dump to the same file.