NFVBENCH-111 Add support for VxLAN
[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     })
319
320 def _get_traffic_client():
321     config = _get_dummy_tg_config('PVP', 'ndr_pdr')
322     config['vxlan'] = False
323     config['ndr_run'] = True
324     config['pdr_run'] = True
325     config['generator_profile'] = 'dummy'
326     config['single_run'] = False
327     traffic_client = TrafficClient(config)
328     traffic_client.start_traffic_generator()
329     traffic_client.set_traffic('64', True)
330     return traffic_client
331
332 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
333 def test_ndr_at_lr():
334     """Test NDR at line rate."""
335     traffic_client = _get_traffic_client()
336     tg = traffic_client.gen
337     # this is a perfect sut with no loss at LR
338     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=100, max_11_tx=100)
339     # tx packets should be line rate for 64B and no drops...
340     assert tg.get_tx_pps_dropped_pps(100) == (LR_64B_PPS, 0)
341     # NDR and PDR should be at 100%
342     traffic_client.ensure_end_to_end()
343     results = traffic_client.get_ndr_and_pdr()
344     assert_ndr_pdr(results, 200.0, 0.0, 200.0, 0.0)
345
346 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
347 def test_ndr_at_50():
348     """Test NDR at 50% line rate.
349
350     This is a sut with an NDR of 50% and linear drop rate after NDR up to 20% drops at LR
351     (meaning that if you send 100% TX, you will only receive 80% RX)
352     the tg requested TX/actual TX ratio is up to 50%, after 50%
353     is linear up 80% actuak TX when requesting 100%
354     """
355     traffic_client = _get_traffic_client()
356     tg = traffic_client.gen
357
358     tg.set_response_curve(lr_dr=20, ndr=50, max_actual_tx=80, max_11_tx=50)
359     # tx packets should be half line rate for 64B and no drops...
360     assert tg.get_tx_pps_dropped_pps(50) == (LR_64B_PPS / 2, 0)
361     # at 100% TX requested, actual TX is 80% where the drop rate is 3/5 of 20% of the actual TX
362     assert tg.get_tx_pps_dropped_pps(100) == (int(LR_64B_PPS * 0.8),
363                                               int(LR_64B_PPS * 0.8 * 0.6 * 0.2))
364     results = traffic_client.get_ndr_and_pdr()
365     assert_ndr_pdr(results, 100.0, 0.0, 100.781, 0.09374)
366
367 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
368 def test_ndr_pdr_low_cpu():
369     """Test NDR and PDR with too low cpu.
370
371     This test is for the case where the TG is underpowered and cannot send fast enough for the NDR
372     true NDR=40%, actual TX at 50% = 30%, actual measured DR is 0%
373     The ndr/pdr should bail out with a warning and a best effort measured NDR of 30%
374     """
375     traffic_client = _get_traffic_client()
376     tg = traffic_client.gen
377     tg.set_response_curve(lr_dr=50, ndr=40, max_actual_tx=60, max_11_tx=0)
378     # tx packets should be 30% at requested half line rate for 64B and no drops...
379     assert tg.get_tx_pps_dropped_pps(50) == (int(LR_64B_PPS * 0.3), 0)
380     results = traffic_client.get_ndr_and_pdr()
381     assert results
382     # import pprint
383     # pp = pprint.PrettyPrinter(indent=4)
384     # pp.pprint(results)
385
386 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
387 def test_no_openstack():
388     """Test nfvbench using main."""
389     config = _get_dummy_tg_config('EXT', '1000pps')
390     config.openrc_file = None
391     config.vlans = [[100], [200]]
392     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
393     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
394     del config['generator_profile']
395     old_argv = sys.argv
396     sys.argv = [old_argv[0], '-c', json.dumps(config)]
397     nfvbench.nfvbench.main()
398     sys.argv = old_argv