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