NFVBENCH-113 Add direct support for trex cores as an cli/config option
[nfvbench.git] / test / test_nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
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
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 #    under the License.
15 #
16 from mock_trex import no_op
17
18 import json
19 import logging
20 import sys
21
22 from attrdict import AttrDict
23 from mock import patch
24 import pytest
25
26 from nfvbench.config import config_loads
27 from nfvbench.credentials import Credentials
28 from nfvbench.fluentd import FluentLogHandler
29 import nfvbench.log
30 import nfvbench.nfvbench
31 from nfvbench.traffic_client import Device
32 from nfvbench.traffic_client import GeneratorConfig
33 from nfvbench.traffic_client import IpBlock
34 from nfvbench.traffic_client import TrafficClient
35 import nfvbench.traffic_gen.traffic_utils as traffic_utils
36
37
38 # just to get rid of the unused function warning
39 no_op()
40
41 def setup_module(module):
42     """Enable log."""
43     nfvbench.log.setup(mute_stdout=True)
44
45 # =========================================================================
46 # Traffic client tests
47 # =========================================================================
48
49 def test_parse_rate_str():
50     parse_rate_str = traffic_utils.parse_rate_str
51     try:
52         assert parse_rate_str('100%') == {'rate_percent': '100.0'}
53         assert parse_rate_str('37.5%') == {'rate_percent': '37.5'}
54         assert parse_rate_str('100%') == {'rate_percent': '100.0'}
55         assert parse_rate_str('60pps') == {'rate_pps': '60'}
56         assert parse_rate_str('60kpps') == {'rate_pps': '60000'}
57         assert parse_rate_str('6Mpps') == {'rate_pps': '6000000'}
58         assert parse_rate_str('6gpps') == {'rate_pps': '6000000000'}
59         assert parse_rate_str('80bps') == {'rate_bps': '80'}
60         assert parse_rate_str('80bps') == {'rate_bps': '80'}
61         assert parse_rate_str('80kbps') == {'rate_bps': '80000'}
62         assert parse_rate_str('80kBps') == {'rate_bps': '640000'}
63         assert parse_rate_str('80Mbps') == {'rate_bps': '80000000'}
64         assert parse_rate_str('80 MBps') == {'rate_bps': '640000000'}
65         assert parse_rate_str('80Gbps') == {'rate_bps': '80000000000'}
66     except Exception as exc:
67         assert False, exc.message
68
69     def should_raise_error(str):
70         try:
71             parse_rate_str(str)
72         except Exception:
73             return True
74         else:
75             return False
76         return False
77
78     assert should_raise_error('101')
79     assert should_raise_error('201%')
80     assert should_raise_error('10Kbps')
81     assert should_raise_error('0kbps')
82     assert should_raise_error('0pps')
83     assert should_raise_error('-1bps')
84
85
86 def test_rate_conversion():
87     assert traffic_utils.load_to_bps(50, 10000000000) == pytest.approx(5000000000.0)
88     assert traffic_utils.load_to_bps(37, 10000000000) == pytest.approx(3700000000.0)
89     assert traffic_utils.load_to_bps(100, 10000000000) == pytest.approx(10000000000.0)
90
91     assert traffic_utils.bps_to_load(5000000000.0, 10000000000) == pytest.approx(50.0)
92     assert traffic_utils.bps_to_load(3700000000.0, 10000000000) == pytest.approx(37.0)
93     assert traffic_utils.bps_to_load(10000000000.0, 10000000000) == pytest.approx(100.0)
94
95     assert traffic_utils.bps_to_pps(500000, 64) == pytest.approx(744.047619048)
96     assert traffic_utils.bps_to_pps(388888, 1518) == pytest.approx(31.6066319896)
97     assert traffic_utils.bps_to_pps(9298322222, 340.3) == pytest.approx(3225895.85831)
98
99     assert traffic_utils.pps_to_bps(744.047619048, 64) == pytest.approx(500000)
100     assert traffic_utils.pps_to_bps(31.6066319896, 1518) == pytest.approx(388888)
101     assert traffic_utils.pps_to_bps(3225895.85831, 340.3) == pytest.approx(9298322222)
102
103
104 # pps at 10Gbps line rate for 64 byte frames
105 LR_64B_PPS = 14880952
106 LR_1518B_PPS = 812743
107
108 def assert_equivalence(reference, value, allowance_pct=1):
109     """Assert if a value is equivalent to a reference value with given margin.
110
111     :param float reference: reference value to compare to
112     :param float value: value to compare to reference
113     :param float allowance_pct: max allowed percentage of margin
114         0 : requires exact match
115         1 : must be equal within 1% of the reference value
116         ...
117         100: always true
118     """
119     if reference == 0:
120         assert value == 0
121     else:
122         assert abs(value - reference) * 100 / reference <= allowance_pct
123
124 def test_load_from_rate():
125     assert traffic_utils.get_load_from_rate('100%') == 100
126     assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_64B_PPS) + 'pps'))
127     assert_equivalence(50, traffic_utils.get_load_from_rate(str(LR_64B_PPS / 2) + 'pps'))
128     assert_equivalence(100, traffic_utils.get_load_from_rate('10Gbps'))
129     assert_equivalence(50, traffic_utils.get_load_from_rate('5000Mbps'))
130     assert_equivalence(1, traffic_utils.get_load_from_rate('100Mbps'))
131     assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS) + 'pps',
132                                                              avg_frame_size=1518))
133     assert_equivalence(100, traffic_utils.get_load_from_rate(str(LR_1518B_PPS * 2) + 'pps',
134                                                              avg_frame_size=1518,
135                                                              line_rate='20Gbps'))
136
137 # =========================================================================
138 # Other tests
139 # =========================================================================
140
141 def test_no_credentials():
142     cred = Credentials('/completely/wrong/path/openrc', None, False)
143     if cred.rc_auth_url:
144         # shouldn't get valid data unless user set environment variables
145         assert False
146     else:
147         assert True
148
149 def test_ip_block():
150     ipb = IpBlock('10.0.0.0', '0.0.0.1', 256)
151     assert ipb.get_ip() == '10.0.0.0'
152     assert ipb.get_ip(255) == '10.0.0.255'
153     with pytest.raises(IndexError):
154         ipb.get_ip(256)
155     # verify with step larger than 1
156     ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
157     assert ipb.get_ip() == '10.0.0.0'
158     assert ipb.get_ip(1) == '10.0.0.2'
159     assert ipb.get_ip(128) == '10.0.1.0'
160     assert ipb.get_ip(255) == '10.0.1.254'
161     with pytest.raises(IndexError):
162         ipb.get_ip(256)
163
164 def check_stream_configs(gen_config):
165     """Verify that the range for each chain have adjacent IP ranges without holes between chains."""
166     config = gen_config.config
167     tgc = config['traffic_generator']
168     step = Device.ip_to_int(tgc['ip_addrs_step'])
169     cfc = 0
170     sip = Device.ip_to_int(tgc['ip_addrs'][0].split('/')[0])
171     dip = Device.ip_to_int(tgc['ip_addrs'][1].split('/')[0])
172     stream_configs = gen_config.devices[0].get_stream_configs()
173     for index in range(config['service_chain_count']):
174         stream_cfg = stream_configs[index]
175         assert stream_cfg['ip_src_count'] == stream_cfg['ip_dst_count']
176         assert Device.ip_to_int(stream_cfg['ip_src_addr']) == sip
177         assert Device.ip_to_int(stream_cfg['ip_dst_addr']) == dip
178         count = stream_cfg['ip_src_count']
179         cfc += count
180         sip += count * step
181         dip += count * step
182     assert cfc == int(config['flow_count'] / 2)
183
184 def _check_device_flow_config(step_ip):
185     config = _get_dummy_tg_config('PVP', '1Mpps', scc=10, fc=99999, step_ip=step_ip)
186     gen_config = GeneratorConfig(config)
187     check_stream_configs(gen_config)
188
189 def test_device_flow_config():
190     _check_device_flow_config('0.0.0.1')
191     _check_device_flow_config('0.0.0.2')
192
193 def test_config():
194     refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
195     res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}
196     res2 = {1: 100, 2: {21: 1000, 22: 200}, 3: None}
197     res3 = {1: 100, 2: {21: 100, 22: 200}, 3: "abc"}
198     assert config_loads("{}", refcfg) == refcfg
199     assert config_loads("{1: 10}", refcfg) == res1
200     assert config_loads("{2: {21: 1000}}", refcfg) == res2
201     assert config_loads('{3: "abc"}', refcfg) == res3
202
203     # correctly fails
204     # pairs of input string and expected subset (None if identical)
205     fail_pairs = [
206         ["{4: 0}", None],
207         ["{2: {21: 100, 30: 50}}", "{2: {30: 50}}"],
208         ["{2: {0: 1, 1: 2}, 5: 5}", None],
209         ["{1: 'abc', 2: {21: 0}}", "{1: 'abc'}"],
210         ["{2: 100}", None]
211     ]
212     for fail_pair in fail_pairs:
213         with pytest.raises(Exception) as e_info:
214             config_loads(fail_pair[0], refcfg)
215         expected = fail_pair[1]
216         if expected is None:
217             expected = fail_pair[0]
218         assert expected in str(e_info)
219
220     # whitelist keys
221     flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
222                          'extra_specs': {'hw:cpu_policy': 'dedicated'}}}
223     new_flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
224                              'extra_specs': {'hw:cpu_policy': 'dedicated', 'hw:numa_nodes': 2}}}
225     assert config_loads("{'flavor': {'extra_specs': {'hw:numa_nodes': 2}}}", flavor,
226                         whitelist_keys=['alpha', 'extra_specs']) == new_flavor
227
228
229 def test_fluentd():
230     logger = logging.getLogger('fluent-logger')
231
232     class FluentdConfig(dict):
233         def __getattr__(self, attr):
234             return self.get(attr)
235
236     fluentd_configs = [
237         FluentdConfig({
238             'logging_tag': 'nfvbench',
239             'result_tag': 'resultnfvbench',
240             'ip': '127.0.0.1',
241             'port': 7081
242         }),
243         FluentdConfig({
244             'logging_tag': 'nfvbench',
245             'result_tag': 'resultnfvbench',
246             'ip': '127.0.0.1',
247             'port': 24224
248         }),
249         FluentdConfig({
250             'logging_tag': None,
251             'result_tag': 'resultnfvbench',
252             'ip': '127.0.0.1',
253             'port': 7082
254         }),
255         FluentdConfig({
256             'logging_tag': 'nfvbench',
257             'result_tag': None,
258             'ip': '127.0.0.1',
259             'port': 7083
260         })
261     ]
262
263     handler = FluentLogHandler(fluentd_configs=fluentd_configs)
264     logger.addHandler(handler)
265     logger.setLevel(logging.INFO)
266     logger.info('test')
267     logger.warning('test %d', 100)
268
269     try:
270         raise Exception("test")
271     except Exception:
272         logger.exception("got exception")
273
274 def assert_ndr_pdr(stats, ndr, ndr_dr, pdr, pdr_dr):
275     assert stats['ndr']['rate_percent'] == ndr
276     assert stats['ndr']['stats']['overall']['drop_percentage'] == ndr_dr
277     assert_equivalence(pdr, stats['pdr']['rate_percent'])
278     assert_equivalence(pdr_dr, stats['pdr']['stats']['overall']['drop_percentage'])
279
280 def _get_dummy_tg_config(chain_type, rate, scc=1, fc=10, step_ip='0.0.0.1',
281                          ip0='10.0.0.0/8', ip1='20.0.0.0/8'):
282     return AttrDict({
283         'traffic_generator': {'host_name': 'nfvbench_tg',
284                               'default_profile': 'dummy',
285                               'generator_profile': [{'name': 'dummy',
286                                                      'tool': 'dummy',
287                                                      'ip': '127.0.0.1',
288                                                      'intf_speed': '10Gbps',
289                                                      'interfaces': [{'port': 0, 'pci': '0.0'},
290                                                                     {'port': 1, 'pci': '0.0'}]}],
291                               'ip_addrs_step': step_ip,
292                               'ip_addrs': [ip0, ip1],
293                               'tg_gateway_ip_addrs': ['1.1.0.100', '2.2.0.100'],
294                               'tg_gateway_ip_addrs_step': step_ip,
295                               'gateway_ip_addrs': ['1.1.0.2', '2.2.0.2'],
296                               'gateway_ip_addrs_step': step_ip,
297                               'mac_addrs_left': None,
298                               'mac_addrs_right': None,
299                               'udp_src_port': None,
300                               'udp_dst_port': None},
301         'traffic': {'profile': 'profile_64',
302                     'bidirectional': True},
303         'traffic_profile': [{'name': 'profile_64', 'l2frame_size': ['64']}],
304         'generator_profile': None,
305         'service_chain': chain_type,
306         'service_chain_count': scc,
307         'flow_count': fc,
308         'vlan_tagging': True,
309         'no_arp': False,
310         'duration_sec': 1,
311         'interval_sec': 1,
312         'pause_sec': 1,
313         'rate': rate,
314         'check_traffic_time_sec': 200,
315         'generic_poll_sec': 2,
316         'measurement': {'NDR': 0.001, 'PDR': 0.1, 'load_epsilon': 0.1},
317         'l2_loopback': False,
318         'cores': None,
319         'mbuf_factor': None
320     })
321
322 def _get_traffic_client():
323     config = _get_dummy_tg_config('PVP', 'ndr_pdr')
324     config['vxlan'] = False
325     config['ndr_run'] = True
326     config['pdr_run'] = True
327     config['generator_profile'] = 'dummy'
328     config['single_run'] = False
329     traffic_client = TrafficClient(config)
330     traffic_client.start_traffic_generator()
331     traffic_client.set_traffic('64', True)
332     return traffic_client
333
334 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
335 def test_ndr_at_lr():
336     """Test NDR at line rate."""
337     traffic_client = _get_traffic_client()
338     tg = traffic_client.gen
339     # this is a perfect sut with no loss at LR
340     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=100, max_11_tx=100)
341     # tx packets should be line rate for 64B and no drops...
342     assert tg.get_tx_pps_dropped_pps(100) == (LR_64B_PPS, 0)
343     # NDR and PDR should be at 100%
344     traffic_client.ensure_end_to_end()
345     results = traffic_client.get_ndr_and_pdr()
346     assert_ndr_pdr(results, 200.0, 0.0, 200.0, 0.0)
347
348 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
349 def test_ndr_at_50():
350     """Test NDR at 50% line rate.
351
352     This is a sut with an NDR of 50% and linear drop rate after NDR up to 20% drops at LR
353     (meaning that if you send 100% TX, you will only receive 80% RX)
354     the tg requested TX/actual TX ratio is up to 50%, after 50%
355     is linear up 80% actuak TX when requesting 100%
356     """
357     traffic_client = _get_traffic_client()
358     tg = traffic_client.gen
359
360     tg.set_response_curve(lr_dr=20, ndr=50, max_actual_tx=80, max_11_tx=50)
361     # tx packets should be half line rate for 64B and no drops...
362     assert tg.get_tx_pps_dropped_pps(50) == (LR_64B_PPS / 2, 0)
363     # at 100% TX requested, actual TX is 80% where the drop rate is 3/5 of 20% of the actual TX
364     assert tg.get_tx_pps_dropped_pps(100) == (int(LR_64B_PPS * 0.8),
365                                               int(LR_64B_PPS * 0.8 * 0.6 * 0.2))
366     results = traffic_client.get_ndr_and_pdr()
367     assert_ndr_pdr(results, 100.0, 0.0, 100.781, 0.09374)
368
369 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
370 def test_ndr_pdr_low_cpu():
371     """Test NDR and PDR with too low cpu.
372
373     This test is for the case where the TG is underpowered and cannot send fast enough for the NDR
374     true NDR=40%, actual TX at 50% = 30%, actual measured DR is 0%
375     The ndr/pdr should bail out with a warning and a best effort measured NDR of 30%
376     """
377     traffic_client = _get_traffic_client()
378     tg = traffic_client.gen
379     tg.set_response_curve(lr_dr=50, ndr=40, max_actual_tx=60, max_11_tx=0)
380     # tx packets should be 30% at requested half line rate for 64B and no drops...
381     assert tg.get_tx_pps_dropped_pps(50) == (int(LR_64B_PPS * 0.3), 0)
382     results = traffic_client.get_ndr_and_pdr()
383     assert results
384     # import pprint
385     # pp = pprint.PrettyPrinter(indent=4)
386     # pp.pprint(results)
387
388 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
389 def test_no_openstack():
390     """Test nfvbench using main."""
391     config = _get_dummy_tg_config('EXT', '1000pps')
392     config.openrc_file = None
393     config.vlans = [[100], [200]]
394     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
395     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
396     del config['generator_profile']
397     old_argv = sys.argv
398     sys.argv = [old_argv[0], '-c', json.dumps(config)]
399     nfvbench.nfvbench.main()
400     sys.argv = old_argv