Skip to content

Commit 7f51580

Browse files
authored
[202503] Backport exabgp fix to 202503 branch (#922)
This PR is to backport exabgp changes from master branch to 202503. PR list - sonic-net/sonic-mgmt#16744 - sonic-net/sonic-mgmt#17427 - sonic-net/sonic-mgmt#16834 - sonic-net/sonic-mgmt#18181 - sonic-net/sonic-mgmt#18476 These changes are required for running test `bgp/test_traffic_shift.py`
2 parents 4418a5b + 071d52c commit 7f51580

File tree

4 files changed

+173
-59
lines changed

4 files changed

+173
-59
lines changed

ansible/library/exabgp.py

Lines changed: 121 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -47,58 +47,51 @@
4747
DEFAULT_BGP_LISTEN_PORT = 179
4848

4949
http_api_py = '''\
50-
from flask import Flask, request
50+
from __future__ import print_function
51+
import tornado.ioloop
52+
import tornado.web
5153
import sys
52-
import six
53-
54-
#Disable banner msg from app.run, or the output might be caught by exabgp and run as command
55-
cli = sys.modules['flask.cli']
56-
cli.show_server_banner = lambda *x: None
57-
58-
app = Flask(__name__)
59-
60-
# Setup a command route to listen for prefix advertisements
61-
@app.route('/', methods=['POST'])
62-
def run_command():
63-
# code made compatible to run in Py2 or Py3 environment
64-
# to support back-porting
65-
request_has_commands = False
66-
if six.PY2:
67-
request_has_commands = request.form.has_key('commands')
68-
else:
69-
request_has_commands = 'commands' in request.form
70-
71-
if request_has_commands:
72-
cmds = request.form['commands'].split(';')
73-
else:
74-
cmds = [ request.form['command'] ]
75-
for cmd in cmds:
76-
sys.stdout.write("%s\\n" % cmd)
77-
sys.stdout.flush()
78-
return "OK\\n"
7954
80-
if __name__ == '__main__':
81-
# with werkzeug 3.x the default size of max_form_memory_size
82-
# is 500K. Routes reach a bit beyond that and the client
83-
# receives HTTP 413.
84-
# Configure the max size to 4 MB to be safe.
85-
if not six.PY2:
86-
from werkzeug import Request
87-
max_content_length = 4 * 1024 * 1024
88-
Request.max_content_length = max_content_length
89-
Request.max_form_memory_size = max_content_length
90-
Request.max_form_parts = max_content_length
91-
app.run(host='0.0.0.0', port=sys.argv[1])
55+
class route_handler(tornado.web.RequestHandler):
56+
def post(self):
57+
# Read the form data
58+
command = self.get_body_argument("command", None)
59+
commands = self.get_body_argument("commands", None)
60+
61+
# Process and print the command values
62+
if command:
63+
out_str = "{}\\n".format(command)
64+
sys.stdout.write(out_str)
65+
if commands:
66+
values = commands.split(';')
67+
for value in values:
68+
out_str = "{}\\n".format(value)
69+
sys.stdout.write(out_str)
70+
71+
sys.stdout.flush()
72+
self.write("OK\\n")
73+
74+
def make_app():
75+
return tornado.web.Application([
76+
(r"/upload", UploadHandler),
77+
])
78+
79+
if __name__ == "__main__":
80+
app = tornado.web.Application([
81+
("/", route_handler),
82+
])
83+
app.listen(int(sys.argv[1]))
84+
tornado.ioloop.IOLoop.current().start()
9285
'''
9386

94-
dump_config_tmpl = '''\
87+
exabgp3_dump_config_tmpl = '''\
9588
process dump {
89+
run /usr/bin/python {{ dump_script }};
9690
encoder json;
9791
receive {
9892
parsed;
9993
update;
10094
}
101-
run /usr/bin/python {{ dump_script }};
10295
}
10396
'''
10497

@@ -132,6 +125,14 @@ def run_command():
132125
# Example configs are available here
133126
# https://github.com/Exa-Networks/exabgp/tree/master/etc/exabgp
134127
# Look for sample for a given section for details
128+
129+
exabgp4_dump_config_tmpl = '''\
130+
process dump {
131+
run /usr/bin/python {{ dump_script }};
132+
encoder json;
133+
}
134+
'''
135+
135136
exabgp4_config_template = '''\
136137
{{ dump_config }}
137138
process http-api {
@@ -149,15 +150,34 @@ def run_command():
149150
passive;
150151
listen {{ listen_port }};
151152
{%- endif %}
152-
api {
153+
api http_api{
153154
processes [ http-api ];
154155
}
156+
{%- if dump_config %}
157+
api dumper {
158+
processes [ dump ];
159+
receive {
160+
parsed;
161+
update;
162+
}
163+
}
164+
{%- endif %}
155165
}
156166
'''
157167

158-
exabgp_supervisord_conf_tmpl = '''\
168+
# Unlike in ExaBGP V3.x, in V4+ the process API is expected to acknowledge
169+
# with 'done' or 'error' string back to ExaBGP. Else the pipe becomes blocked
170+
# and ExaBGP will hang. Alternatively the acknowledgement can be disabled.
171+
# https://github.com/Exa-Networks/exabgp/wiki/Migration-from-3.4-to-4.x#api
172+
exabgp_v4_env_tmpl = '''\
173+
[exabgp.api]
174+
ack = false
175+
'''
176+
177+
exabgp_supervisord_conf_tmpl_p1 = '''\
159178
[program:exabgp-{{ name }}]
160-
command=/usr/local/bin/exabgp /etc/exabgp/{{ name }}.conf
179+
'''
180+
exabgp_supervisord_conf_tmpl_p3 = '''\
161181
stdout_logfile=/tmp/exabgp-{{ name }}.out.log
162182
stderr_logfile=/tmp/exabgp-{{ name }}.err.log
163183
stdout_logfile_maxbytes=10000000
@@ -170,6 +190,18 @@ def run_command():
170190
startsecs=1
171191
numprocs=1
172192
'''
193+
exabgp_supervisord_conf_tmpl_p2_v3 = '''\
194+
command=/usr/local/bin/exabgp /etc/exabgp/{{ name }}.conf
195+
'''
196+
exabgp_supervisord_conf_tmpl_p2_v3_debug = '''\
197+
command=/usr/local/bin/exabgp --debug /etc/exabgp/{{ name }}.conf
198+
'''
199+
exabgp_supervisord_conf_tmpl_p2_v4 = '''\
200+
command=/usr/local/bin/exabgp --env /etc/exabgp/exabgp.env /etc/exabgp/{{ name }}.conf
201+
'''
202+
exabgp_supervisord_conf_tmpl_p2_v4_debug = '''\
203+
command=/usr/local/bin/exabgp --debug --env /etc/exabgp/exabgp.env /etc/exabgp/{{ name }}.conf
204+
'''
173205

174206

175207
def exec_command(module, cmd, ignore_error=False, msg="executing command"):
@@ -234,8 +266,12 @@ def setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn, peer_asn, p
234266

235267
dump_config = ""
236268
if dump_script:
237-
dump_config = jinja2.Template(
238-
dump_config_tmpl).render(dump_script=dump_script)
269+
if six.PY2:
270+
dump_config = jinja2.Template(
271+
exabgp3_dump_config_tmpl).render(dump_script=dump_script)
272+
else:
273+
dump_config = jinja2.Template(
274+
exabgp4_dump_config_tmpl).render(dump_script=dump_script)
239275

240276
# backport friendly checking; not required if everything is Py3
241277
t = None
@@ -259,14 +295,42 @@ def setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn, peer_asn, p
259295
out_file.write(data)
260296

261297

298+
def setup_exabgp_env():
299+
try:
300+
os.mkdir("/etc/exabgp", 0o755)
301+
except OSError:
302+
pass
303+
with open("/etc/exabgp/exabgp.env", 'w') as out_file:
304+
out_file.write(exabgp_v4_env_tmpl)
305+
306+
262307
def remove_exabgp_conf(name):
263308
try:
264309
os.remove("/etc/exabgp/%s.conf" % name)
265310
except Exception:
266311
pass
267312

268313

269-
def setup_exabgp_supervisord_conf(name):
314+
def setup_exabgp_supervisord_conf(name, debug=False):
315+
exabgp_supervisord_conf_tmpl = None
316+
if six.PY2:
317+
if debug:
318+
exabgp_supervisord_conf_tmpl = exabgp_supervisord_conf_tmpl_p1 + \
319+
exabgp_supervisord_conf_tmpl_p2_v3_debug + \
320+
exabgp_supervisord_conf_tmpl_p3
321+
else:
322+
exabgp_supervisord_conf_tmpl = exabgp_supervisord_conf_tmpl_p1 + \
323+
exabgp_supervisord_conf_tmpl_p2_v3 + \
324+
exabgp_supervisord_conf_tmpl_p3
325+
else:
326+
if debug:
327+
exabgp_supervisord_conf_tmpl = exabgp_supervisord_conf_tmpl_p1 + \
328+
exabgp_supervisord_conf_tmpl_p2_v4_debug + \
329+
exabgp_supervisord_conf_tmpl_p3
330+
else:
331+
exabgp_supervisord_conf_tmpl = exabgp_supervisord_conf_tmpl_p1 + \
332+
exabgp_supervisord_conf_tmpl_p2_v4 + \
333+
exabgp_supervisord_conf_tmpl_p3
270334
t = jinja2.Template(exabgp_supervisord_conf_tmpl)
271335
data = t.render(name=name)
272336
with open("/etc/supervisor/conf.d/exabgp-%s.conf" % name, 'w') as out_file:
@@ -302,7 +366,8 @@ def main():
302366
peer_asn=dict(required=False, type='int'),
303367
port=dict(required=False, type='int', default=5000),
304368
dump_script=dict(required=False, type='str', default=None),
305-
passive=dict(required=False, type='bool', default=False)
369+
passive=dict(required=False, type='bool', default=False),
370+
debug=dict(required=False, type='bool', default=False)
306371
),
307372
supports_check_mode=False)
308373

@@ -316,32 +381,35 @@ def main():
316381
port = module.params['port']
317382
dump_script = module.params['dump_script']
318383
passive = module.params['passive']
384+
debug = module.params['debug']
319385

320386
setup_exabgp_processor()
387+
if not six.PY2:
388+
setup_exabgp_env()
321389

322390
result = {}
323391
try:
324392
if state == 'started':
325393
setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn,
326394
peer_asn, port, dump_script=dump_script, passive=passive)
327-
setup_exabgp_supervisord_conf(name)
395+
setup_exabgp_supervisord_conf(name, debug=debug)
328396
refresh_supervisord(module)
329397
start_exabgp(module, name)
330398
elif state == 'restarted':
331399
setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn,
332400
peer_asn, port, dump_script=dump_script, passive=passive)
333-
setup_exabgp_supervisord_conf(name)
401+
setup_exabgp_supervisord_conf(name, debug=debug)
334402
refresh_supervisord(module)
335403
restart_exabgp(module, name)
336404
elif state == 'present':
337405
setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn,
338406
peer_asn, port, dump_script=dump_script, passive=passive)
339-
setup_exabgp_supervisord_conf(name)
407+
setup_exabgp_supervisord_conf(name, debug=debug)
340408
refresh_supervisord(module)
341409
elif state == 'configure':
342410
setup_exabgp_conf(name, router_id, local_ip, peer_ip, local_asn,
343411
peer_asn, port, dump_script=dump_script, passive=passive)
344-
setup_exabgp_supervisord_conf(name)
412+
setup_exabgp_supervisord_conf(name, debug=debug)
345413
elif state == 'stopped':
346414
stop_exabgp(module, name)
347415
elif state == 'absent':

tests/bgp/bgp_monitor_dump.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@
22

33
from sys import stdin
44
import json
5-
65
DUMP_FILE = "/tmp/bgp_monitor_dump.log"
76

7+
# The announcement format is different between exabgp v3 and v4
8+
# The default value is set to 'v3' if the version cannot be determined
9+
# from the message
10+
exabgp_version = 'v3'
11+
ver_found = False
12+
813
while True:
914
with open(DUMP_FILE, "a") as f:
1015
line = stdin.readline()
1116
obj = json.loads(line)
17+
ver = obj.get('exabgp')
18+
if ver and not ver_found:
19+
ver_found = True
20+
if ver.startswith('4'):
21+
exabgp_version = 'v4'
1222
if 'update' not in obj['neighbor']['message']:
1323
continue
1424
announce = obj['neighbor']['message']['update']['announce']
1525
keys = ('ipv4 unicast', 'ipv6 unicast')
1626
for key in keys:
1727
if key in announce:
1828
for _, route in list(announce[key].items()):
19-
for ip, _ in list(route.items()):
20-
f.write(ip + "\n")
29+
if exabgp_version == 'v3':
30+
for ip, _ in list(route.items()):
31+
f.write(ip + "\n")
32+
elif exabgp_version == 'v4':
33+
route_list = route
34+
for r in route_list:
35+
for msg_type, ip in r.items():
36+
if msg_type == 'nlri':
37+
f.write(ip + "\n")

tests/common/helpers/bgp.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class BGPNeighbor(object):
5959
def __init__(self, duthost, ptfhost, name,
6060
neighbor_ip, neighbor_asn,
6161
dut_ip, dut_asn, port, neigh_type=None,
62-
namespace=None, is_multihop=False, is_passive=False):
62+
namespace=None, is_multihop=False, is_passive=False, debug=False):
6363
self.duthost = duthost
6464
self.ptfhost = ptfhost
6565
self.ptfip = ptfhost.mgmt_ip
@@ -73,6 +73,7 @@ def __init__(self, duthost, ptfhost, name,
7373
self.namespace = namespace
7474
self.is_passive = is_passive
7575
self.is_multihop = not is_passive and is_multihop
76+
self.debug = debug
7677

7778
def start_session(self):
7879
"""Start the BGP session."""
@@ -111,7 +112,8 @@ def start_session(self):
111112
peer_ip=self.peer_ip,
112113
local_asn=self.asn,
113114
peer_asn=self.peer_asn,
114-
port=self.port
115+
port=self.port,
116+
debug=self.debug
115117
)
116118
if not wait_tcp_connection(self.ptfhost, self.ptfip, self.port, timeout_s=60):
117119
raise RuntimeError("Failed to start BGP neighbor %s" % self.name)

tests/dualtor/test_orchagent_slb.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import ipaddress
2+
import logging
3+
import os.path
24
import pytest
35
import random
46
import time
@@ -144,6 +146,9 @@ def _find_ipv6_vlan(mg_facts):
144146
ptfhost.shell("ip route add %s via %s" % (conn["local_addr"], vlan_intf_addr))
145147
yield connections
146148
finally:
149+
upper_tor_host.shell("show arp")
150+
lower_tor_host.shell("show arp")
151+
ptfhost.shell("ip route show")
147152
for conn in list(connections.values()):
148153
ptfhost.shell("ifconfig %s 0.0.0.0" % conn["neighbor_intf"], module_ignore_errors=True)
149154
ptfhost.shell("ip route del %s" % conn["local_addr"], module_ignore_errors=True)
@@ -165,11 +170,33 @@ def bgp_neighbors(ptfhost, setup_interfaces):
165170
conn["local_addr"].split("/")[0],
166171
conn["local_asn"],
167172
conn["exabgp_port"],
168-
is_passive=True
173+
is_passive=True,
174+
debug=True
169175
)
170176
return neighbors
171177

172178

179+
@pytest.fixture(scope="module", autouse=True)
180+
def save_slb_exabgp_logfiles(ptfhost, pytestconfig, request):
181+
"""Save slb exabgp log files to the log directory."""
182+
# remove log files before test
183+
log_files_before = ptfhost.shell("ls /tmp/exabgp-slb_*.log")["stdout"].split()
184+
for log_file in log_files_before:
185+
ptfhost.file(path=log_file, state="absent")
186+
187+
yield
188+
189+
test_log_file = pytestconfig.getoption("log_file", None)
190+
if test_log_file:
191+
log_dir = os.path.dirname(os.path.abspath(test_log_file))
192+
log_files = ptfhost.shell("ls /tmp/exabgp-slb_*.log")["stdout"].split()
193+
for log_file in log_files:
194+
logging.debug("Save slb exabgp log %s to %s", log_file, log_dir)
195+
ptfhost.fetch(src=log_file, dest=log_dir + os.path.sep, fail_on_missing=False, flat=True)
196+
else:
197+
logging.info("Skip saving slb exabgp log files to log directory as log directory not set.")
198+
199+
173200
@pytest.fixture(params=['ipv4', 'ipv6'])
174201
def ip_version(request):
175202
"""Traffic IP version to test."""

0 commit comments

Comments
 (0)