forked from sonic-net/sonic-mgmt
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfib_test.py
More file actions
431 lines (374 loc) · 19.3 KB
/
Copy pathfib_test.py
File metadata and controls
431 lines (374 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
'''
Description: This file contains the FIB test for SONIC
Design is available in https://github.com/Azure/SONiC/wiki/FIB-Scale-Test-Plan
Usage: Examples of how to use log analyzer
ptf --test-dir ptftests fib_test.FibTest --platform-dir ptftests --qlen=2000 --platform remote -t 'setup_info="/root/test_fib_setup_info.json";testbed_mtu=1514;ipv4=True;test_balancing=True;ipv6=True' --relax --debug info --log-file /tmp/fib_test.FibTest.ipv4.True.ipv6.True.2020-12-22-08:17:05.log --socket-recv-size 16384
'''
#---------------------------------------------------------------------
# Global imports
#---------------------------------------------------------------------
import logging
import random
import time
import json
import ptf
import ptf.packet as scapy
from ptf import config
from ptf.base_tests import BaseTest
from ptf.mask import Mask
from ptf.testutils import test_params_get
from ptf.testutils import simple_tcp_packet
from ptf.testutils import simple_tcpv6_packet
from ptf.testutils import send_packet
from ptf.testutils import verify_packet_any_port
from ptf.testutils import verify_no_packet_any
import fib
class FibTest(BaseTest):
'''
@summary: Overview of functionality
Test routes advertised by BGP peers of SONIC are working properly.
The setup of peers is described in 'VM set' section in
https://github.com/Azure/sonic-mgmt/blob/master/docs/ansible/README.testbed.md
Routes advertized by the peers have ECMP groups. The purpose of the test is to make sure
that packets are forwarded through one of the ports specified in route's ECMP group.
This class receives a text file describing the bgp routes added to the switch.
File contains information about each bgp route which was added to the switch.
#-----------------------------------------------------------------------
The file is loaded on startup and is used to
- construct packet with correct destination IP
- validate that packet arrived from switch from a port which
is member of ECMP group for given route.
For each route test
- builds a packet with destination IP matching to the IP in the route
- sends packet to the switch
- verifies that packet came back from the switch on one of
the ports specified in the ECMP group of the route.
'''
#---------------------------------------------------------------------
# Class variables
#---------------------------------------------------------------------
DEFAULT_BALANCING_RANGE = 0.25
BALANCING_TEST_TIMES = 625
DEFAULT_BALANCING_TEST_NUMBER = 1
ACTION_FWD = 'fwd'
ACTION_DROP = 'drop'
_required_params = [
'fib_info_files',
'ptf_test_port_map',
'router_macs'
]
def __init__(self):
'''
@summary: constructor
'''
BaseTest.__init__(self)
self.test_params = test_params_get()
self.check_required_params()
def check_required_params(self):
for param in self._required_params:
if param not in self.test_params:
raise Exception("Missing required parameter {}".format(param))
def setUp(self):
'''
@summary: Setup for the test
Some test parameters are used:
- setup_info: various configuration required by the FIB ptf test
- src_ports: this list should include all enabled ports, both up links
and down links.
- pkt_action: expect to receive test traffic or not. Default: fwd
- ipv4/ipv6: enable ipv4/ipv6 tests
Other test parameters:
- ttl: ttl of test pkts. Auto decrease 1 for expected pkts.
- ip_options enable ip option header in ipv4 pkts. Default: False(disable)
- src_vid vlan tag id of src pkts. Default: None(untag)
- dst_vid vlan tag id of dst pkts. Default: None(untag)
- ignore_ttl: mask the ttl field in the expected packet
- single_fib_for_duts: have a single fib file for all DUTs in multi-dut case. Default: False
'''
self.dataplane = ptf.dataplane_instance
self.fibs = []
for fib_info_file in self.test_params.get('fib_info_files'):
self.fibs.append(fib.Fib(fib_info_file))
ptf_test_port_map = self.test_params.get('ptf_test_port_map')
with open(ptf_test_port_map) as f:
self.ptf_test_port_map = json.load(f)
self.router_macs = self.test_params.get('router_macs')
self.pktlen = self.test_params.get('testbed_mtu', 1500)
self.test_ipv4 = self.test_params.get('ipv4', True)
self.test_ipv6 = self.test_params.get('ipv6', True)
self.test_balancing = self.test_params.get('test_balancing', True)
self.balancing_range = self.test_params.get('balancing_range', self.DEFAULT_BALANCING_RANGE)
self.balancing_test_times = self.test_params.get('balancing_test_times', self.BALANCING_TEST_TIMES)
self.balancing_test_number = self.test_params.get('balancing_test_number', self.DEFAULT_BALANCING_TEST_NUMBER)
self.balancing_test_count = 0
self.pkt_action = self.test_params.get('pkt_action', self.ACTION_FWD)
self.ttl = self.test_params.get('ttl', 64)
self.ip_options = self.test_params.get('ip_options', False)
self.src_vid = self.test_params.get('src_vid', None)
self.dst_vid = self.test_params.get('dst_vid', None)
self.src_ports = self.test_params.get('src_ports', None)
if not self.src_ports:
self.src_ports = [int(port) for port in self.ptf_test_port_map.keys()]
self.ignore_ttl = self.test_params.get('ignore_ttl', False)
self.single_fib = self.test_params.get('single_fib_for_duts', False)
def check_ip_ranges(self, ipv4=True):
for dut_index, fib in enumerate(self.fibs):
if ipv4:
ip_ranges = fib.ipv4_ranges()
else:
ip_ranges = fib.ipv6_ranges()
if len(ip_ranges) > 150:
covered_ip_ranges = ip_ranges[:100] + random.sample(ip_ranges[100:], 50) # Limit test execution time
else:
covered_ip_ranges = ip_ranges[:]
for ip_range in covered_ip_ranges:
if ip_range.get_first_ip() in fib:
self.check_ip_range(ip_range, dut_index, ipv4)
random.shuffle(covered_ip_ranges)
self.check_balancing(covered_ip_ranges, dut_index, ipv4)
def get_src_and_exp_ports(self, dst_ip):
while True:
src_port = int(random.choice(self.src_ports))
if self.single_fib:
active_dut_index = 0
else:
active_dut_index = self.ptf_test_port_map[str(src_port)]['target_dut']
next_hop = self.fibs[active_dut_index][dst_ip]
exp_port_list = next_hop.get_next_hop_list()
if src_port in exp_port_list:
continue
break
return src_port, exp_port_list, next_hop
def check_ip_range(self, ip_range, dut_index, ipv4=True):
dst_ips = []
dst_ips.append(ip_range.get_first_ip())
if ip_range.length > 1:
dst_ips.append(ip_range.get_last_ip())
if ip_range.length > 2:
dst_ips.append(ip_range.get_random_ip())
for dst_ip in dst_ips:
src_port, exp_ports, _ = self.get_src_and_exp_ports(dst_ip)
if not exp_ports:
logging.info('Skip checking ip range {} with exp_ports {}'.format(ip_range, exp_ports))
return
logging.info('Checking ip range {}, src_port={}, exp_ports={}, dst_ip={}, dut_index={}'\
.format(ip_range, src_port, exp_ports, dst_ip, dut_index))
self.check_ip_route(src_port, dst_ip, exp_ports, ipv4)
def check_balancing(self, ip_ranges, dut_index, ipv4=True):
# Test traffic balancing across ECMP/LAG members
if self.test_balancing and self.pkt_action == self.ACTION_FWD:
for ip_range in ip_ranges:
dst_ip = ip_range.get_random_ip()
src_port, exp_port_list, next_hop = self.get_src_and_exp_ports(dst_ip)
if len(exp_port_list) <= 1:
# Only 1 expected output port is not enough for balancing test.
continue
hit_count_map = {}
# Change balancing_test_times according to number of next hop groups
logging.info('Checking ip range balancing {}, src_port={}, exp_ports={}, dst_ip={}, dut_index={}'\
.format(ip_range, src_port, exp_port_list, dst_ip, dut_index))
for i in range(0, self.balancing_test_times*len(exp_port_list)):
(matched_index, received) = self.check_ip_route(src_port, dst_ip, exp_port_list, ipv4)
hit_count_map[matched_index] = hit_count_map.get(matched_index, 0) + 1
self.check_hit_count_map(next_hop.get_next_hop(), hit_count_map)
self.balancing_test_count += 1
if self.balancing_test_count >= self.balancing_test_number:
break
def check_ip_route(self, src_port, dst_ip_addr, dst_port_list, ipv4=True):
if ipv4:
res = self.check_ipv4_route(src_port, dst_ip_addr, dst_port_list)
else:
res = self.check_ipv6_route(src_port, dst_ip_addr, dst_port_list)
if self.pkt_action == self.ACTION_DROP:
return res
(matched_index, received) = res
assert received
matched_port = dst_port_list[matched_index]
logging.info("Received packet at " + str(matched_port))
time.sleep(0.02)
return (matched_port, received)
def check_ipv4_route(self, src_port, dst_ip_addr, dst_port_list):
'''
@summary: Check IPv4 route works.
@param src_port: index of port to use for sending packet to switch
@param dest_ip_addr: destination IP to build packet with.
@param dst_port_list: list of ports on which to expect packet to come back from the switch
'''
sport = random.randint(0, 65535)
dport = random.randint(0, 65535)
ip_src = "30.0.0.1"
ip_dst = dst_ip_addr
src_mac = self.dataplane.get_mac(0, src_port)
router_mac = self.ptf_test_port_map[str(src_port)]['target_mac']
pkt = simple_tcp_packet(
pktlen=self.pktlen,
eth_dst=router_mac,
eth_src=src_mac,
ip_src=ip_src,
ip_dst=ip_dst,
tcp_sport=sport,
tcp_dport=dport,
ip_ttl=self.ttl,
ip_options=self.ip_options,
dl_vlan_enable=self.src_vid is not None,
vlan_vid=self.src_vid or 0)
exp_pkt = simple_tcp_packet(
self.pktlen,
ip_src=ip_src,
ip_dst=ip_dst,
tcp_sport=sport,
tcp_dport=dport,
ip_ttl=max(self.ttl-1, 0),
ip_options=self.ip_options,
dl_vlan_enable=self.dst_vid is not None,
vlan_vid=self.dst_vid or 0)
masked_exp_pkt = Mask(exp_pkt)
masked_exp_pkt.set_do_not_care_scapy(scapy.Ether, "dst")
masked_exp_pkt.set_do_not_care_scapy(scapy.Ether, "src")
# mask the chksum also if masking the ttl
if self.ignore_ttl:
masked_exp_pkt.set_do_not_care_scapy(scapy.IP, "ttl")
masked_exp_pkt.set_do_not_care_scapy(scapy.IP, "chksum")
masked_exp_pkt.set_do_not_care_scapy(scapy.TCP, "chksum")
send_packet(self, src_port, pkt)
logging.info('Sent Ether(src={}, dst={})/IP(src={}, dst={})/TCP(sport={}, dport={}) on port {}'\
.format(pkt.src,
pkt.dst,
pkt['IP'].src,
pkt['IP'].dst,
sport,
dport,
src_port))
logging.info('Expect Ether(src={}, dst={})/IP(src={}, dst={})/TCP(sport={}, dport={})'\
.format('any',
'any',
ip_src,
ip_dst,
sport,
dport))
if self.pkt_action == self.ACTION_FWD:
rcvd_port, rcvd_pkt = verify_packet_any_port(self,masked_exp_pkt,dst_port_list)
exp_src_mac = self.router_macs[self.ptf_test_port_map[str(dst_port_list[rcvd_port])]['target_dut']]
actual_src_mac = Ether(rcvd_pkt).src
if exp_src_mac != actual_src_mac:
raise Exception("Pkt sent from {} to {} on port {} was rcvd pkt on {} which is one of the expected ports, "
"but the src mac doesn't match, expected {}, got {}".
format(ip_src, ip_dst, src_port, dst_port_list[rcvd_port], exp_src_mac, actual_src_mac))
return (rcvd_port, rcvd_pkt)
elif self.pkt_action == self.ACTION_DROP:
return verify_no_packet_any(self, masked_exp_pkt, dst_port_list)
#---------------------------------------------------------------------
def check_ipv6_route(self, src_port, dst_ip_addr, dst_port_list):
'''
@summary: Check IPv6 route works.
@param source_port_index: index of port to use for sending packet to switch
@param dest_ip_addr: destination IP to build packet with.
@param dst_port_list: list of ports on which to expect packet to come back from the switch
@return Boolean
'''
sport = random.randint(0, 65535)
dport = random.randint(0, 65535)
ip_src = '2000:0030::1'
ip_dst = dst_ip_addr
src_mac = self.dataplane.get_mac(0, src_port)
router_mac = self.ptf_test_port_map[str(src_port)]['target_mac']
pkt = simple_tcpv6_packet(
pktlen=self.pktlen,
eth_dst=router_mac,
eth_src=src_mac,
ipv6_dst=ip_dst,
ipv6_src=ip_src,
tcp_sport=sport,
tcp_dport=dport,
ipv6_hlim=self.ttl,
dl_vlan_enable=self.src_vid is not None,
vlan_vid=self.src_vid or 0)
exp_pkt = simple_tcpv6_packet(
pktlen=self.pktlen,
ipv6_dst=ip_dst,
ipv6_src=ip_src,
tcp_sport=sport,
tcp_dport=dport,
ipv6_hlim=max(self.ttl-1, 0),
dl_vlan_enable=self.dst_vid is not None,
vlan_vid=self.dst_vid or 0)
masked_exp_pkt = Mask(exp_pkt)
masked_exp_pkt.set_do_not_care_scapy(scapy.Ether,"dst")
masked_exp_pkt.set_do_not_care_scapy(scapy.Ether,"src")
# mask the chksum also if masking the ttl
if self.ignore_ttl:
masked_exp_pkt.set_do_not_care_scapy(scapy.IPv6, "hlim")
masked_exp_pkt.set_do_not_care_scapy(scapy.IPv6, "chksum")
masked_exp_pkt.set_do_not_care_scapy(scapy.TCP, "chksum")
send_packet(self, src_port, pkt)
logging.info('Sent Ether(src={}, dst={})/IPv6(src={}, dst={})/TCP(sport={}, dport={})'\
.format(pkt.src,
pkt.dst,
pkt['IPv6'].src,
pkt['IPv6'].dst,
sport,
dport))
logging.info('Expect Ether(src={}, dst={})/IPv6(src={}, dst={})/TCP(sport={}, dport={})'\
.format('any',
'any',
ip_src,
ip_dst,
sport,
dport))
if self.pkt_action == self.ACTION_FWD:
rcvd_port, rcvd_pkt = verify_packet_any_port(self, masked_exp_pkt, dst_port_list)
exp_src_mac = self.router_macs[self.ptf_test_port_map[str(dst_port_list[rcvd_port])]['target_dut']]
actual_src_mac = Ether(rcvd_pkt).src
if actual_src_mac != exp_src_mac:
raise Exception("Pkt sent from {} to {} on port {} was rcvd pkt on {} which is one of the expected ports, "
"but the src mac doesn't match, expected {}, got {}".
format(ip_src, ip_dst, src_port, dst_port_list[rcvd_port], exp_src_mac, actual_src_mac))
return (rcvd_port, rcvd_pkt)
elif self.pkt_action == self.ACTION_DROP:
return verify_no_packet_any(self, masked_exp_pkt, dst_port_list)
def check_within_expected_range(self, actual, expected):
'''
@summary: Check if the actual number is within the accepted range of the expected number
@param actual : acutal number of recieved packets
@param expected : expected number of recieved packets
@return (percentage, bool)
'''
percentage = (actual - expected) / float(expected)
return (percentage, abs(percentage) <= self.balancing_range)
def check_hit_count_map(self, dest_port_list, port_hit_cnt):
'''
@summary: Check if the traffic is balanced across the ECMP groups and the LAG members
@param dest_port_list : a list of ECMP entries and in each ECMP entry a list of ports
@param port_hit_cnt : a dict that records the number of packets each port received
@return bool
'''
logging.info("%-10s \t %-10s \t %10s \t %10s \t %10s" % ("type", "port(s)", "exp_cnt", "act_cnt", "diff(%)"))
result = True
total_hit_cnt = sum(port_hit_cnt.values())
for ecmp_entry in dest_port_list:
total_entry_hit_cnt = 0
for member in ecmp_entry:
total_entry_hit_cnt += port_hit_cnt.get(member, 0)
(p, r) = self.check_within_expected_range(total_entry_hit_cnt, float(total_hit_cnt)/len(dest_port_list))
logging.info("%-10s \t %-10s \t %10d \t %10d \t %10s"
% ("ECMP", str(ecmp_entry), total_hit_cnt//len(dest_port_list), total_entry_hit_cnt, str(round(p, 4)*100) + '%'))
result &= r
if len(ecmp_entry) == 1 or total_entry_hit_cnt == 0:
continue
for member in ecmp_entry:
(p, r) = self.check_within_expected_range(port_hit_cnt.get(member, 0), float(total_entry_hit_cnt)/len(ecmp_entry))
logging.info("%-10s \t %-10s \t %10d \t %10d \t %10s"
% ("LAG", str(member), total_entry_hit_cnt//len(ecmp_entry), port_hit_cnt.get(member, 0), str(round(p, 4)*100) + '%'))
result &= r
assert result
def runTest(self):
"""
@summary: Send packet for each range of both IPv4 and IPv6 spaces and
expect the packet to be received from one of the expected ports
"""
# IPv4 Test
if (self.test_ipv4):
self.check_ip_ranges()
# IPv6 Test
if (self.test_ipv6):
self.check_ip_ranges(ipv4=False)