2 # Copyright 2016 Cisco Systems, Inc. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
16 from mock import patch
19 from .mock_trex import no_op
24 from attrdict import AttrDict
25 from nfvbench.config import config_loads
26 from nfvbench.credentials import Credentials
27 from nfvbench.fluentd import FluentLogHandler
29 import nfvbench.nfvbench
30 from nfvbench.traffic_client import Device
31 from nfvbench.traffic_client import GeneratorConfig
32 from nfvbench.traffic_client import IpBlock
33 from nfvbench.traffic_client import TrafficClient
34 import nfvbench.traffic_gen.traffic_utils as traffic_utils
37 # just to get rid of the unused function warning
40 def setup_module(module):
42 nfvbench.log.setup(mute_stdout=True)
44 # =========================================================================
45 # Traffic client tests
46 # =========================================================================
48 def test_parse_rate_str():
49 parse_rate_str = traffic_utils.parse_rate_str
51 assert parse_rate_str('100%') == {'rate_percent': '100.0'}
52 assert parse_rate_str('37.5%') == {'rate_percent': '37.5'}
53 assert parse_rate_str('100%') == {'rate_percent': '100.0'}
54 assert parse_rate_str('60pps') == {'rate_pps': '60'}
55 assert parse_rate_str('60kpps') == {'rate_pps': '60000'}
56 assert parse_rate_str('6Mpps') == {'rate_pps': '6000000'}
57 assert parse_rate_str('6gpps') == {'rate_pps': '6000000000'}
58 assert parse_rate_str('80bps') == {'rate_bps': '80'}
59 assert parse_rate_str('80bps') == {'rate_bps': '80'}
60 assert parse_rate_str('80kbps') == {'rate_bps': '80000'}
61 assert parse_rate_str('80kBps') == {'rate_bps': '640000'}
62 assert parse_rate_str('80Mbps') == {'rate_bps': '80000000'}
63 assert parse_rate_str('80 MBps') == {'rate_bps': '640000000'}
64 assert parse_rate_str('80Gbps') == {'rate_bps': '80000000000'}
65 except Exception as exc:
66 assert False, exc.message
68 def should_raise_error(str):
77 assert should_raise_error('101')
78 assert should_raise_error('201%')
79 assert should_raise_error('10Kbps')
80 assert should_raise_error('0kbps')
81 assert should_raise_error('0pps')
82 assert should_raise_error('-1bps')
85 def test_rate_conversion():
86 assert traffic_utils.load_to_bps(50, 10000000000) == pytest.approx(5000000000.0)
87 assert traffic_utils.load_to_bps(37, 10000000000) == pytest.approx(3700000000.0)
88 assert traffic_utils.load_to_bps(100, 10000000000) == pytest.approx(10000000000.0)
90 assert traffic_utils.bps_to_load(5000000000.0, 10000000000) == pytest.approx(50.0)
91 assert traffic_utils.bps_to_load(3700000000.0, 10000000000) == pytest.approx(37.0)
92 assert traffic_utils.bps_to_load(10000000000.0, 10000000000) == pytest.approx(100.0)
94 assert traffic_utils.bps_to_pps(500000, 64) == pytest.approx(744.047619048)
95 assert traffic_utils.bps_to_pps(388888, 1518) == pytest.approx(31.6066319896)
96 assert traffic_utils.bps_to_pps(9298322222, 340.3) == pytest.approx(3225895.85831)
98 assert traffic_utils.pps_to_bps(744.047619048, 64) == pytest.approx(500000)
99 assert traffic_utils.pps_to_bps(31.6066319896, 1518) == pytest.approx(388888)
100 assert traffic_utils.pps_to_bps(3225895.85831, 340.3) == pytest.approx(9298322222)
103 # pps at 10Gbps line rate for 64 byte frames
104 LR_64B_PPS = 14880952
105 LR_1518B_PPS = 812743
107 def assert_equivalence(reference, value, allowance_pct=1):
108 """Assert if a value is equivalent to a reference value with given margin.
110 :param float reference: reference value to compare to
111 :param float value: value to compare to reference
112 :param float allowance_pct: max allowed percentage of margin
113 0 : requires exact match
114 1 : must be equal within 1% of the reference value
121 assert abs(value - reference) * 100 / reference <= allowance_pct
123 def test_load_from_rate():
124 assert traffic_utils.get_load_from_rate('100%') == 100
125 assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_64B_PPS) + 'pps'))
126 assert_equivalence(50, traffic_utils.get_load_from_rate(str(LR_64B_PPS / 2) + 'pps'))
127 assert_equivalence(100, traffic_utils.get_load_from_rate('10Gbps'))
128 assert_equivalence(50, traffic_utils.get_load_from_rate('5000Mbps'))
129 assert_equivalence(1, traffic_utils.get_load_from_rate('100Mbps'))
130 assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS) + 'pps',
131 avg_frame_size=1518))
132 assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS * 2) + 'pps',
136 # =========================================================================
138 # =========================================================================
140 def test_no_credentials():
141 cred = Credentials('/completely/wrong/path/openrc', None, False)
143 # shouldn't get valid data unless user set environment variables
149 ipb = IpBlock('10.0.0.0', '0.0.0.1', 256)
150 assert ipb.get_ip() == '10.0.0.0'
151 assert ipb.get_ip(255) == '10.0.0.255'
152 with pytest.raises(IndexError):
154 ipb = IpBlock('10.0.0.0', '0.0.0.1', 1)
155 assert ipb.get_ip() == '10.0.0.0'
156 with pytest.raises(IndexError):
159 ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
160 assert ipb.get_ip() == '10.0.0.0'
161 assert ipb.get_ip(1) == '10.0.0.2'
162 assert ipb.get_ip(127) == '10.0.0.254'
163 assert ipb.get_ip(128) == '10.0.1.0'
164 with pytest.raises(IndexError):
167 # verify with step larger than 1
168 ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
169 assert ipb.get_ip() == '10.0.0.0'
170 assert ipb.get_ip(1) == '10.0.0.2'
171 assert ipb.get_ip(128) == '10.0.1.0'
172 assert ipb.get_ip(255) == '10.0.1.254'
173 with pytest.raises(IndexError):
176 ipb = IpBlock('10.0.0.0', '0.0.0.2', 128)
177 assert ipb.get_ip() == '10.0.0.0'
178 assert ipb.get_ip(1) == '10.0.0.2'
179 assert ipb.get_ip(127) == '10.0.0.254'
180 with pytest.raises(IndexError):
183 ipb = IpBlock('10.0.0.0', '0.0.0.4', 64)
184 assert ipb.get_ip() == '10.0.0.0'
185 assert ipb.get_ip(1) == '10.0.0.4'
186 assert ipb.get_ip(63) == '10.0.0.252'
187 with pytest.raises(IndexError):
190 ipb = IpBlock('10.0.0.0', '0.0.0.10', 1)
191 assert ipb.get_ip() == '10.0.0.0'
192 with pytest.raises(IndexError):
197 assert Device.lcm(10, 2) == 10
198 assert Device.lcm(1, 256) == 256
199 assert Device.lcm(10, 256) == 1280
200 assert Device.lcm(Device.lcm(10, 2), Device.lcm(1, 256))
201 with pytest.raises(TypeError):
205 def test_check_ipsize():
206 assert Device.check_ipsize(256, 1) == 256
207 assert Device.check_ipsize(256, 3) == 86
208 assert Device.check_ipsize(256, 4) == 64
209 assert Device.check_ipsize(16, 10) == 2
210 assert Device.check_ipsize(1, 10) == 1
211 with pytest.raises(ZeroDivisionError):
212 Device.check_ipsize(256, 0)
215 def test_reserve_ip_range():
216 ipb = IpBlock('10.0.0.0', '0.0.0.1', 256)
217 src_ip_first, src_ip_last = ipb.reserve_ip_range(256, False)
218 assert src_ip_first == "10.0.0.0"
219 assert src_ip_last == "10.0.0.255"
220 ipb = IpBlock('20.0.0.0', '0.0.0.1', 2)
221 src_ip_first, src_ip_last = ipb.reserve_ip_range(2, False)
222 assert src_ip_first == "20.0.0.0"
223 assert src_ip_last == "20.0.0.1"
224 ipb = IpBlock('30.0.0.0', '0.0.0.1', 2)
225 src_ip_first, src_ip_last = ipb.reserve_ip_range(256, True)
226 assert src_ip_first == "30.0.0.0"
227 assert src_ip_last == "30.0.0.1"
228 ipb = IpBlock('40.0.0.0', '0.0.0.1', 2)
229 with pytest.raises(IndexError):
230 ipb.reserve_ip_range(256, False)
233 def check_stream_configs(gen_config):
234 """Verify that the range for each chain have adjacent IP ranges without holes between chains."""
235 config = gen_config.config
236 tgc = config['traffic_generator']
237 step = Device.ip_to_int(tgc['ip_addrs_step'])
239 sip = Device.ip_to_int(tgc['ip_addrs'][0].split('/')[0])
240 dip = Device.ip_to_int(tgc['ip_addrs'][1].split('/')[0])
241 stream_configs = gen_config.devices[0].get_stream_configs()
242 for index in range(config['service_chain_count']):
243 stream_cfg = stream_configs[index]
244 assert stream_cfg['ip_src_count'] == stream_cfg['ip_dst_count']
245 assert Device.ip_to_int(stream_cfg['ip_src_addr']) == sip
246 assert Device.ip_to_int(stream_cfg['ip_dst_addr']) == dip
247 count = stream_cfg['ip_src_count']
251 assert cfc == int(config['flow_count'] / 2)
253 def _check_device_flow_config(step_ip):
254 config = _get_dummy_tg_config('PVP', '1Mpps', scc=10, fc=99999, step_ip=step_ip)
255 gen_config = GeneratorConfig(config)
256 check_stream_configs(gen_config)
258 def test_device_flow_config():
259 _check_device_flow_config('0.0.0.1')
260 _check_device_flow_config('0.0.0.2')
263 def check_udp_stream_configs(gen_config, expected):
264 """Verify that the range for each chain have adjacent UDP ports without holes between chains."""
265 config = gen_config.config
266 stream_configs = gen_config.devices[0].get_stream_configs()
267 for index in range(config['service_chain_count']):
268 stream_cfg = stream_configs[index]
269 assert stream_cfg['ip_src_addr'] == expected['ip_src_addr']
270 assert stream_cfg['ip_src_addr_max'] == expected['ip_src_addr_max']
271 assert stream_cfg['ip_dst_addr'] == expected['ip_dst_addr']
272 assert stream_cfg['ip_dst_addr_max'] == expected['ip_dst_addr_max']
273 assert stream_cfg['udp_src_port'] == expected['udp_src_port']
274 assert stream_cfg['udp_src_port_max'] == expected['udp_src_port_max']
275 assert stream_cfg['udp_src_count'] == expected['udp_src_count']
276 assert stream_cfg['udp_dst_port'] == expected['udp_dst_port']
277 assert stream_cfg['udp_dst_port_max'] == expected['udp_dst_port_max']
278 assert stream_cfg['udp_dst_count'] == expected['udp_dst_count']
281 def _check_device_udp_flow_config(param, expected):
282 config = _get_dummy_tg_config('PVP', '1Mpps',
283 ip_src_static=param['ip_src_static'],
284 fc=param['flow_count'],
285 ip0=param['ip_src_addr'],
286 ip1=param['ip_dst_addr'],
287 src_udp=param['udp_src_port'],
288 dst_udp=param['udp_dst_port'])
289 gen_config = GeneratorConfig(config)
290 check_udp_stream_configs(gen_config, expected)
293 def test_device_udp_flow_config():
294 param = {'ip_src_static': True,
295 'ip_src_addr': '110.0.0.0/32',
296 'ip_dst_addr': '120.0.0.0/32',
300 expected = {'ip_src_addr': '110.0.0.0',
301 'ip_src_addr_max': '110.0.0.0',
302 'ip_dst_addr': '120.0.0.0',
303 'ip_dst_addr_max': '120.0.0.0',
305 'udp_src_port_max': 53,
308 'udp_dst_port_max': 53,
310 _check_device_udp_flow_config(param, expected)
311 # Overwrite the udp_src_port value to define a large range of ports
312 # instead of a single port, in order to check if the imposed
313 # flow count is respected. Notice that udp range >> flow count.
314 param['udp_src_port'] = [53, 1024]
315 param['flow_count'] = 10
316 expected['udp_src_port_max'] = 57
317 expected['udp_src_count'] = 5
318 _check_device_udp_flow_config(param, expected)
319 # Re affect the default udp_src_port values and
320 # overwrite the ip_dst_addr value to define a large range of addresses
321 # instead of a single one, in order to check if the imposed
322 # flow count is respected. Notice that the netmask allows a very larger
323 # range of possible addresses than the flow count value.
324 param['udp_src_port'] = 53
325 expected['udp_src_port_max'] = 53
326 expected['udp_src_count'] = 1
327 param['ip_src_static'] = False
328 param['ip_dst_addr'] = '120.0.0.0/24'
329 expected['ip_dst_addr'] = '120.0.0.0'
330 expected['ip_dst_addr_max'] = '120.0.0.4'
331 _check_device_udp_flow_config(param, expected)
335 refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
336 res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}
337 res2 = {1: 100, 2: {21: 1000, 22: 200}, 3: None}
338 res3 = {1: 100, 2: {21: 100, 22: 200}, 3: "abc"}
339 assert config_loads("{}", refcfg) == refcfg
340 assert config_loads("{1: 10}", refcfg) == res1
341 assert config_loads("{2: {21: 1000}}", refcfg) == res2
342 assert config_loads('{3: "abc"}', refcfg) == res3
345 # pairs of input string and expected subset (None if identical)
348 ["{2: {21: 100, 30: 50}}", "{2: {30: 50}}"],
349 ["{2: {0: 1, 1: 2}, 5: 5}", None],
350 ["{1: 'abc', 2: {21: 0}}", "{1: 'abc'}"],
353 for fail_pair in fail_pairs:
354 with pytest.raises(Exception) as e_info:
355 config_loads(fail_pair[0], refcfg)
356 expected = fail_pair[1]
358 expected = fail_pair[0]
359 assert expected in str(e_info)
362 flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
363 'extra_specs': {'hw:cpu_policy': 'dedicated'}}}
364 new_flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
365 'extra_specs': {'hw:cpu_policy': 'dedicated', 'hw:numa_nodes': 2}}}
366 assert config_loads("{'flavor': {'extra_specs': {'hw:numa_nodes': 2}}}", flavor,
367 whitelist_keys=['alpha', 'extra_specs']) == new_flavor
371 logger = logging.getLogger('fluent-logger')
373 class FluentdConfig(dict):
374 def __getattr__(self, attr):
375 return self.get(attr)
379 'logging_tag': 'nfvbench',
380 'result_tag': 'resultnfvbench',
385 'logging_tag': 'nfvbench',
386 'result_tag': 'resultnfvbench',
392 'result_tag': 'resultnfvbench',
397 'logging_tag': 'nfvbench',
404 handler = FluentLogHandler(fluentd_configs=fluentd_configs)
405 logger.addHandler(handler)
406 logger.setLevel(logging.INFO)
408 logger.warning('test %d', 100)
411 raise Exception("test")
413 logger.exception("got exception")
415 def assert_ndr_pdr(stats, ndr, ndr_dr, pdr, pdr_dr):
416 assert stats['ndr']['rate_percent'] == ndr
417 assert stats['ndr']['stats']['overall']['drop_percentage'] == ndr_dr
418 assert_equivalence(pdr, stats['pdr']['rate_percent'])
419 assert_equivalence(pdr_dr, stats['pdr']['stats']['overall']['drop_percentage'])
421 def _get_dummy_tg_config(chain_type, rate, scc=1, fc=10, step_ip='0.0.0.1',
422 ip0='10.0.0.0/8', ip1='20.0.0.0/8',
423 step_udp=1, src_udp=None, dst_udp=None, ip_src_static=True):
425 'traffic_generator': {'host_name': 'nfvbench_tg',
426 'default_profile': 'dummy',
427 'generator_profile': [{'name': 'dummy',
430 'intf_speed': '10Gbps',
431 'interfaces': [{'port': 0, 'pci': '0.0'},
432 {'port': 1, 'pci': '0.0'}]}],
433 'ip_addrs_step': step_ip,
434 'ip_addrs': [ip0, ip1],
435 'ip_src_static': ip_src_static,
436 'tg_gateway_ip_addrs': ['1.1.0.100', '2.2.0.100'],
437 'tg_gateway_ip_addrs_step': step_ip,
438 'gateway_ip_addrs': ['1.1.0.2', '2.2.0.2'],
439 'gateway_ip_addrs_step': step_ip,
440 'mac_addrs_left': None,
441 'mac_addrs_right': None,
442 'udp_src_port': src_udp,
443 'udp_dst_port': dst_udp,
444 'udp_port_step': step_udp},
445 'traffic': {'profile': 'profile_64',
446 'bidirectional': True},
447 'traffic_profile': [{'name': 'profile_64', 'l2frame_size': ['64']}],
448 'generator_profile': None,
449 'service_chain': chain_type,
450 'service_chain_count': scc,
452 'vlan_tagging': True,
458 'check_traffic_time_sec': 200,
459 'generic_poll_sec': 2,
460 'measurement': {'NDR': 0.001, 'PDR': 0.1, 'load_epsilon': 0.1},
461 'l2_loopback': False,
464 'disable_hdrh': None,
466 'service_mode': False,
467 'no_flow_stats': False,
468 'no_latency_stats': False,
469 'no_latency_streams': False
473 def _get_traffic_client():
474 config = _get_dummy_tg_config('PVP', 'ndr_pdr')
475 config['vxlan'] = False
476 config['mpls'] = False
477 config['ndr_run'] = True
478 config['pdr_run'] = True
479 config['generator_profile'] = 'dummy'
480 config['single_run'] = False
481 traffic_client = TrafficClient(config)
482 traffic_client.start_traffic_generator()
483 traffic_client.set_traffic('64', True)
484 return traffic_client
486 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
487 def test_ndr_at_lr():
488 """Test NDR at line rate."""
489 traffic_client = _get_traffic_client()
490 tg = traffic_client.gen
491 # this is a perfect sut with no loss at LR
492 tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=100, max_11_tx=100)
493 # tx packets should be line rate for 64B and no drops...
494 assert tg.get_tx_pps_dropped_pps(100) == (LR_64B_PPS, 0)
495 # NDR and PDR should be at 100%
496 # traffic_client.ensure_end_to_end()
497 results = traffic_client.get_ndr_and_pdr()
498 assert_ndr_pdr(results, 200.0, 0.0, 200.0, 0.0)
500 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
501 def test_ndr_at_50():
502 """Test NDR at 50% line rate.
504 This is a sut with an NDR of 50% and linear drop rate after NDR up to 20% drops at LR
505 (meaning that if you send 100% TX, you will only receive 80% RX)
506 the tg requested TX/actual TX ratio is up to 50%, after 50%
507 is linear up 80% actuak TX when requesting 100%
509 traffic_client = _get_traffic_client()
510 tg = traffic_client.gen
512 tg.set_response_curve(lr_dr=20, ndr=50, max_actual_tx=80, max_11_tx=50)
513 # tx packets should be half line rate for 64B and no drops...
514 assert tg.get_tx_pps_dropped_pps(50) == (LR_64B_PPS / 2, 0)
515 # at 100% TX requested, actual TX is 80% where the drop rate is 3/5 of 20% of the actual TX
516 assert tg.get_tx_pps_dropped_pps(100) == (int(LR_64B_PPS * 0.8),
517 int(LR_64B_PPS * 0.8 * 0.6 * 0.2))
518 results = traffic_client.get_ndr_and_pdr()
519 assert_ndr_pdr(results, 100.0, 0.0, 100.781, 0.09374)
521 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
522 def test_ndr_pdr_low_cpu():
523 """Test NDR and PDR with too low cpu.
525 This test is for the case where the TG is underpowered and cannot send fast enough for the NDR
526 true NDR=40%, actual TX at 50% = 30%, actual measured DR is 0%
527 The ndr/pdr should bail out with a warning and a best effort measured NDR of 30%
529 traffic_client = _get_traffic_client()
530 tg = traffic_client.gen
531 tg.set_response_curve(lr_dr=50, ndr=40, max_actual_tx=60, max_11_tx=0)
532 # tx packets should be 30% at requested half line rate for 64B and no drops...
533 assert tg.get_tx_pps_dropped_pps(50) == (int(LR_64B_PPS * 0.3), 0)
534 results = traffic_client.get_ndr_and_pdr()
537 # pp = pprint.PrettyPrinter(indent=4)
540 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
541 def test_no_openstack():
542 """Test nfvbench using main."""
543 config = _get_dummy_tg_config('EXT', '1000pps')
544 config.openrc_file = None
545 config.vlans = [[100], [200]]
546 config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
547 config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
548 del config['generator_profile']
550 sys.argv = [old_argv[0], '-c', json.dumps(config)]
551 nfvbench.nfvbench.main()