NFVBENCH-155 Add options to disable extra stats, latency stats and latency streams
[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     ipb = IpBlock('10.0.0.0', '0.0.0.1', 1)
156     assert ipb.get_ip() == '10.0.0.0'
157     with pytest.raises(IndexError):
158         ipb.get_ip(1)
159
160     ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
161     assert ipb.get_ip() == '10.0.0.0'
162     assert ipb.get_ip(1) == '10.0.0.2'
163     assert ipb.get_ip(127) == '10.0.0.254'
164     assert ipb.get_ip(128) == '10.0.1.0'
165     with pytest.raises(IndexError):
166         ipb.get_ip(256)
167
168     # verify with step larger than 1
169     ipb = IpBlock('10.0.0.0', '0.0.0.2', 256)
170     assert ipb.get_ip() == '10.0.0.0'
171     assert ipb.get_ip(1) == '10.0.0.2'
172     assert ipb.get_ip(128) == '10.0.1.0'
173     assert ipb.get_ip(255) == '10.0.1.254'
174     with pytest.raises(IndexError):
175         ipb.get_ip(256)
176
177 def check_stream_configs(gen_config):
178     """Verify that the range for each chain have adjacent IP ranges without holes between chains."""
179     config = gen_config.config
180     tgc = config['traffic_generator']
181     step = Device.ip_to_int(tgc['ip_addrs_step'])
182     cfc = 0
183     sip = Device.ip_to_int(tgc['ip_addrs'][0].split('/')[0])
184     dip = Device.ip_to_int(tgc['ip_addrs'][1].split('/')[0])
185     stream_configs = gen_config.devices[0].get_stream_configs()
186     for index in range(config['service_chain_count']):
187         stream_cfg = stream_configs[index]
188         assert stream_cfg['ip_src_count'] == stream_cfg['ip_dst_count']
189         assert Device.ip_to_int(stream_cfg['ip_src_addr']) == sip
190         assert Device.ip_to_int(stream_cfg['ip_dst_addr']) == dip
191         count = stream_cfg['ip_src_count']
192         cfc += count
193         sip += count * step
194         dip += count * step
195     assert cfc == int(config['flow_count'] / 2)
196
197 def _check_device_flow_config(step_ip):
198     config = _get_dummy_tg_config('PVP', '1Mpps', scc=10, fc=99999, step_ip=step_ip)
199     gen_config = GeneratorConfig(config)
200     check_stream_configs(gen_config)
201
202 def test_device_flow_config():
203     _check_device_flow_config('0.0.0.1')
204     _check_device_flow_config('0.0.0.2')
205
206 def test_config():
207     refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
208     res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}
209     res2 = {1: 100, 2: {21: 1000, 22: 200}, 3: None}
210     res3 = {1: 100, 2: {21: 100, 22: 200}, 3: "abc"}
211     assert config_loads("{}", refcfg) == refcfg
212     assert config_loads("{1: 10}", refcfg) == res1
213     assert config_loads("{2: {21: 1000}}", refcfg) == res2
214     assert config_loads('{3: "abc"}', refcfg) == res3
215
216     # correctly fails
217     # pairs of input string and expected subset (None if identical)
218     fail_pairs = [
219         ["{4: 0}", None],
220         ["{2: {21: 100, 30: 50}}", "{2: {30: 50}}"],
221         ["{2: {0: 1, 1: 2}, 5: 5}", None],
222         ["{1: 'abc', 2: {21: 0}}", "{1: 'abc'}"],
223         ["{2: 100}", None]
224     ]
225     for fail_pair in fail_pairs:
226         with pytest.raises(Exception) as e_info:
227             config_loads(fail_pair[0], refcfg)
228         expected = fail_pair[1]
229         if expected is None:
230             expected = fail_pair[0]
231         assert expected in str(e_info)
232
233     # whitelist keys
234     flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
235                          'extra_specs': {'hw:cpu_policy': 'dedicated'}}}
236     new_flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
237                              'extra_specs': {'hw:cpu_policy': 'dedicated', 'hw:numa_nodes': 2}}}
238     assert config_loads("{'flavor': {'extra_specs': {'hw:numa_nodes': 2}}}", flavor,
239                         whitelist_keys=['alpha', 'extra_specs']) == new_flavor
240
241
242 def test_fluentd():
243     logger = logging.getLogger('fluent-logger')
244
245     class FluentdConfig(dict):
246         def __getattr__(self, attr):
247             return self.get(attr)
248
249     fluentd_configs = [
250         FluentdConfig({
251             'logging_tag': 'nfvbench',
252             'result_tag': 'resultnfvbench',
253             'ip': '127.0.0.1',
254             'port': 7081
255         }),
256         FluentdConfig({
257             'logging_tag': 'nfvbench',
258             'result_tag': 'resultnfvbench',
259             'ip': '127.0.0.1',
260             'port': 24224
261         }),
262         FluentdConfig({
263             'logging_tag': None,
264             'result_tag': 'resultnfvbench',
265             'ip': '127.0.0.1',
266             'port': 7082
267         }),
268         FluentdConfig({
269             'logging_tag': 'nfvbench',
270             'result_tag': None,
271             'ip': '127.0.0.1',
272             'port': 7083
273         })
274     ]
275
276     handler = FluentLogHandler(fluentd_configs=fluentd_configs)
277     logger.addHandler(handler)
278     logger.setLevel(logging.INFO)
279     logger.info('test')
280     logger.warning('test %d', 100)
281
282     try:
283         raise Exception("test")
284     except Exception:
285         logger.exception("got exception")
286
287 def assert_ndr_pdr(stats, ndr, ndr_dr, pdr, pdr_dr):
288     assert stats['ndr']['rate_percent'] == ndr
289     assert stats['ndr']['stats']['overall']['drop_percentage'] == ndr_dr
290     assert_equivalence(pdr, stats['pdr']['rate_percent'])
291     assert_equivalence(pdr_dr, stats['pdr']['stats']['overall']['drop_percentage'])
292
293 def _get_dummy_tg_config(chain_type, rate, scc=1, fc=10, step_ip='0.0.0.1',
294                          ip0='10.0.0.0/8', ip1='20.0.0.0/8'):
295     return AttrDict({
296         'traffic_generator': {'host_name': 'nfvbench_tg',
297                               'default_profile': 'dummy',
298                               'generator_profile': [{'name': 'dummy',
299                                                      'tool': 'dummy',
300                                                      'ip': '127.0.0.1',
301                                                      'intf_speed': '10Gbps',
302                                                      'interfaces': [{'port': 0, 'pci': '0.0'},
303                                                                     {'port': 1, 'pci': '0.0'}]}],
304                               'ip_addrs_step': step_ip,
305                               'ip_addrs': [ip0, ip1],
306                               'tg_gateway_ip_addrs': ['1.1.0.100', '2.2.0.100'],
307                               'tg_gateway_ip_addrs_step': step_ip,
308                               'gateway_ip_addrs': ['1.1.0.2', '2.2.0.2'],
309                               'gateway_ip_addrs_step': step_ip,
310                               'mac_addrs_left': None,
311                               'mac_addrs_right': None,
312                               'udp_src_port': None,
313                               'udp_dst_port': None},
314         'traffic': {'profile': 'profile_64',
315                     'bidirectional': True},
316         'traffic_profile': [{'name': 'profile_64', 'l2frame_size': ['64']}],
317         'generator_profile': None,
318         'service_chain': chain_type,
319         'service_chain_count': scc,
320         'flow_count': fc,
321         'vlan_tagging': True,
322         'no_arp': False,
323         'duration_sec': 1,
324         'interval_sec': 1,
325         'pause_sec': 1,
326         'rate': rate,
327         'check_traffic_time_sec': 200,
328         'generic_poll_sec': 2,
329         'measurement': {'NDR': 0.001, 'PDR': 0.1, 'load_epsilon': 0.1},
330         'l2_loopback': False,
331         'cores': None,
332         'mbuf_factor': None,
333         'disable_hdrh': None,
334         'mbuf_64': None,
335         'service_mode': False,
336         'no_flow_stats': False,
337         'no_latency_stats': False,
338         'no_latency_streams': False
339
340     })
341
342 def _get_traffic_client():
343     config = _get_dummy_tg_config('PVP', 'ndr_pdr')
344     config['vxlan'] = False
345     config['ndr_run'] = True
346     config['pdr_run'] = True
347     config['generator_profile'] = 'dummy'
348     config['single_run'] = False
349     traffic_client = TrafficClient(config)
350     traffic_client.start_traffic_generator()
351     traffic_client.set_traffic('64', True)
352     return traffic_client
353
354 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
355 def test_ndr_at_lr():
356     """Test NDR at line rate."""
357     traffic_client = _get_traffic_client()
358     tg = traffic_client.gen
359     # this is a perfect sut with no loss at LR
360     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=100, max_11_tx=100)
361     # tx packets should be line rate for 64B and no drops...
362     assert tg.get_tx_pps_dropped_pps(100) == (LR_64B_PPS, 0)
363     # NDR and PDR should be at 100%
364     # traffic_client.ensure_end_to_end()
365     results = traffic_client.get_ndr_and_pdr()
366     assert_ndr_pdr(results, 200.0, 0.0, 200.0, 0.0)
367
368 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
369 def test_ndr_at_50():
370     """Test NDR at 50% line rate.
371
372     This is a sut with an NDR of 50% and linear drop rate after NDR up to 20% drops at LR
373     (meaning that if you send 100% TX, you will only receive 80% RX)
374     the tg requested TX/actual TX ratio is up to 50%, after 50%
375     is linear up 80% actuak TX when requesting 100%
376     """
377     traffic_client = _get_traffic_client()
378     tg = traffic_client.gen
379
380     tg.set_response_curve(lr_dr=20, ndr=50, max_actual_tx=80, max_11_tx=50)
381     # tx packets should be half line rate for 64B and no drops...
382     assert tg.get_tx_pps_dropped_pps(50) == (LR_64B_PPS / 2, 0)
383     # at 100% TX requested, actual TX is 80% where the drop rate is 3/5 of 20% of the actual TX
384     assert tg.get_tx_pps_dropped_pps(100) == (int(LR_64B_PPS * 0.8),
385                                               int(LR_64B_PPS * 0.8 * 0.6 * 0.2))
386     results = traffic_client.get_ndr_and_pdr()
387     assert_ndr_pdr(results, 100.0, 0.0, 100.781, 0.09374)
388
389 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
390 def test_ndr_pdr_low_cpu():
391     """Test NDR and PDR with too low cpu.
392
393     This test is for the case where the TG is underpowered and cannot send fast enough for the NDR
394     true NDR=40%, actual TX at 50% = 30%, actual measured DR is 0%
395     The ndr/pdr should bail out with a warning and a best effort measured NDR of 30%
396     """
397     traffic_client = _get_traffic_client()
398     tg = traffic_client.gen
399     tg.set_response_curve(lr_dr=50, ndr=40, max_actual_tx=60, max_11_tx=0)
400     # tx packets should be 30% at requested half line rate for 64B and no drops...
401     assert tg.get_tx_pps_dropped_pps(50) == (int(LR_64B_PPS * 0.3), 0)
402     results = traffic_client.get_ndr_and_pdr()
403     assert results
404     # import pprint
405     # pp = pprint.PrettyPrinter(indent=4)
406     # pp.pprint(results)
407
408 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
409 def test_no_openstack():
410     """Test nfvbench using main."""
411     config = _get_dummy_tg_config('EXT', '1000pps')
412     config.openrc_file = None
413     config.vlans = [[100], [200]]
414     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
415     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
416     del config['generator_profile']
417     old_argv = sys.argv
418     sys.argv = [old_argv[0], '-c', json.dumps(config)]
419     nfvbench.nfvbench.main()
420     sys.argv = old_argv