MPLS support + loop_vm_arp test fix
[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 import json
17 import logging
18 import sys
19
20 from attrdict import AttrDict
21 from mock import patch
22 import pytest
23
24 from nfvbench.config import config_loads
25 from nfvbench.credentials import Credentials
26 from nfvbench.fluentd import FluentLogHandler
27 import nfvbench.log
28 import nfvbench.nfvbench
29 from nfvbench.traffic_client import Device
30 from nfvbench.traffic_client import GeneratorConfig
31 from nfvbench.traffic_client import IpBlock
32 from nfvbench.traffic_client import TrafficClient
33 import nfvbench.traffic_gen.traffic_utils as traffic_utils
34
35 from .mock_trex import no_op
36
37 # just to get rid of the unused function warning
38 no_op()
39
40 def setup_module(module):
41     """Enable log."""
42     nfvbench.log.setup(mute_stdout=True)
43
44 # =========================================================================
45 # Traffic client tests
46 # =========================================================================
47
48 def test_parse_rate_str():
49     parse_rate_str = traffic_utils.parse_rate_str
50     try:
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
67
68     def should_raise_error(str):
69         try:
70             parse_rate_str(str)
71         except Exception:
72             return True
73         else:
74             return False
75         return False
76
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')
83
84
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)
89
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)
93
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)
97
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)
101
102
103 # pps at 10Gbps line rate for 64 byte frames
104 LR_64B_PPS = 14880952
105 LR_1518B_PPS = 812743
106
107 def assert_equivalence(reference, value, allowance_pct=1):
108     """Assert if a value is equivalent to a reference value with given margin.
109
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
115         ...
116         100: always true
117     """
118     if reference == 0:
119         assert value == 0
120     else:
121         assert abs(value - reference) * 100 / reference <= allowance_pct
122
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',
133                                                              avg_frame_size=1518,
134                                                              line_rate='20Gbps'))
135
136 # =========================================================================
137 # Other tests
138 # =========================================================================
139
140 def test_no_credentials():
141     cred = Credentials('/completely/wrong/path/openrc', None, False)
142     if cred.rc_auth_url:
143         # shouldn't get valid data unless user set environment variables
144         assert False
145     else:
146         assert True
147
148 def test_ip_block():
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):
153         ipb.get_ip(256)
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):
157         ipb.get_ip(1)
158
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):
165         ipb.get_ip(256)
166
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):
174         ipb.get_ip(256)
175
176 def check_stream_configs(gen_config):
177     """Verify that the range for each chain have adjacent IP ranges without holes between chains."""
178     config = gen_config.config
179     tgc = config['traffic_generator']
180     step = Device.ip_to_int(tgc['ip_addrs_step'])
181     cfc = 0
182     sip = Device.ip_to_int(tgc['ip_addrs'][0].split('/')[0])
183     dip = Device.ip_to_int(tgc['ip_addrs'][1].split('/')[0])
184     stream_configs = gen_config.devices[0].get_stream_configs()
185     for index in range(config['service_chain_count']):
186         stream_cfg = stream_configs[index]
187         assert stream_cfg['ip_src_count'] == stream_cfg['ip_dst_count']
188         assert Device.ip_to_int(stream_cfg['ip_src_addr']) == sip
189         assert Device.ip_to_int(stream_cfg['ip_dst_addr']) == dip
190         count = stream_cfg['ip_src_count']
191         cfc += count
192         sip += count * step
193         dip += count * step
194     assert cfc == int(config['flow_count'] / 2)
195
196 def _check_device_flow_config(step_ip):
197     config = _get_dummy_tg_config('PVP', '1Mpps', scc=10, fc=99999, step_ip=step_ip)
198     gen_config = GeneratorConfig(config)
199     check_stream_configs(gen_config)
200
201 def test_device_flow_config():
202     _check_device_flow_config('0.0.0.1')
203     _check_device_flow_config('0.0.0.2')
204
205 def test_config():
206     refcfg = {1: 100, 2: {21: 100, 22: 200}, 3: None}
207     res1 = {1: 10, 2: {21: 100, 22: 200}, 3: None}
208     res2 = {1: 100, 2: {21: 1000, 22: 200}, 3: None}
209     res3 = {1: 100, 2: {21: 100, 22: 200}, 3: "abc"}
210     assert config_loads("{}", refcfg) == refcfg
211     assert config_loads("{1: 10}", refcfg) == res1
212     assert config_loads("{2: {21: 1000}}", refcfg) == res2
213     assert config_loads('{3: "abc"}', refcfg) == res3
214
215     # correctly fails
216     # pairs of input string and expected subset (None if identical)
217     fail_pairs = [
218         ["{4: 0}", None],
219         ["{2: {21: 100, 30: 50}}", "{2: {30: 50}}"],
220         ["{2: {0: 1, 1: 2}, 5: 5}", None],
221         ["{1: 'abc', 2: {21: 0}}", "{1: 'abc'}"],
222         ["{2: 100}", None]
223     ]
224     for fail_pair in fail_pairs:
225         with pytest.raises(Exception) as e_info:
226             config_loads(fail_pair[0], refcfg)
227         expected = fail_pair[1]
228         if expected is None:
229             expected = fail_pair[0]
230         assert expected in str(e_info)
231
232     # whitelist keys
233     flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
234                          'extra_specs': {'hw:cpu_policy': 'dedicated'}}}
235     new_flavor = {'flavor': {'vcpus': 2, 'ram': 8192, 'disk': 0,
236                              'extra_specs': {'hw:cpu_policy': 'dedicated', 'hw:numa_nodes': 2}}}
237     assert config_loads("{'flavor': {'extra_specs': {'hw:numa_nodes': 2}}}", flavor,
238                         whitelist_keys=['alpha', 'extra_specs']) == new_flavor
239
240
241 def test_fluentd():
242     logger = logging.getLogger('fluent-logger')
243
244     class FluentdConfig(dict):
245         def __getattr__(self, attr):
246             return self.get(attr)
247
248     fluentd_configs = [
249         FluentdConfig({
250             'logging_tag': 'nfvbench',
251             'result_tag': 'resultnfvbench',
252             'ip': '127.0.0.1',
253             'port': 7081
254         }),
255         FluentdConfig({
256             'logging_tag': 'nfvbench',
257             'result_tag': 'resultnfvbench',
258             'ip': '127.0.0.1',
259             'port': 24224
260         }),
261         FluentdConfig({
262             'logging_tag': None,
263             'result_tag': 'resultnfvbench',
264             'ip': '127.0.0.1',
265             'port': 7082
266         }),
267         FluentdConfig({
268             'logging_tag': 'nfvbench',
269             'result_tag': None,
270             'ip': '127.0.0.1',
271             'port': 7083
272         })
273     ]
274
275     handler = FluentLogHandler(fluentd_configs=fluentd_configs)
276     logger.addHandler(handler)
277     logger.setLevel(logging.INFO)
278     logger.info('test')
279     logger.warning('test %d', 100)
280
281     try:
282         raise Exception("test")
283     except Exception:
284         logger.exception("got exception")
285
286 def assert_ndr_pdr(stats, ndr, ndr_dr, pdr, pdr_dr):
287     assert stats['ndr']['rate_percent'] == ndr
288     assert stats['ndr']['stats']['overall']['drop_percentage'] == ndr_dr
289     assert_equivalence(pdr, stats['pdr']['rate_percent'])
290     assert_equivalence(pdr_dr, stats['pdr']['stats']['overall']['drop_percentage'])
291
292 def _get_dummy_tg_config(chain_type, rate, scc=1, fc=10, step_ip='0.0.0.1',
293                          ip0='10.0.0.0/8', ip1='20.0.0.0/8'):
294     return AttrDict({
295         'traffic_generator': {'host_name': 'nfvbench_tg',
296                               'default_profile': 'dummy',
297                               'generator_profile': [{'name': 'dummy',
298                                                      'tool': 'dummy',
299                                                      'ip': '127.0.0.1',
300                                                      'intf_speed': '10Gbps',
301                                                      'interfaces': [{'port': 0, 'pci': '0.0'},
302                                                                     {'port': 1, 'pci': '0.0'}]}],
303                               'ip_addrs_step': step_ip,
304                               'ip_addrs': [ip0, ip1],
305                               'tg_gateway_ip_addrs': ['1.1.0.100', '2.2.0.100'],
306                               'tg_gateway_ip_addrs_step': step_ip,
307                               'gateway_ip_addrs': ['1.1.0.2', '2.2.0.2'],
308                               'gateway_ip_addrs_step': step_ip,
309                               'mac_addrs_left': None,
310                               'mac_addrs_right': None,
311                               'udp_src_port': None,
312                               'udp_dst_port': None},
313         'traffic': {'profile': 'profile_64',
314                     'bidirectional': True},
315         'traffic_profile': [{'name': 'profile_64', 'l2frame_size': ['64']}],
316         'generator_profile': None,
317         'service_chain': chain_type,
318         'service_chain_count': scc,
319         'flow_count': fc,
320         'vlan_tagging': True,
321         'no_arp': False,
322         'duration_sec': 1,
323         'interval_sec': 1,
324         'pause_sec': 1,
325         'rate': rate,
326         'check_traffic_time_sec': 200,
327         'generic_poll_sec': 2,
328         'measurement': {'NDR': 0.001, 'PDR': 0.1, 'load_epsilon': 0.1},
329         'l2_loopback': False,
330         'cores': None,
331         'mbuf_factor': None,
332         'disable_hdrh': None,
333         'mbuf_64': None,
334         'service_mode': False,
335         'no_flow_stats': False,
336         'no_latency_stats': False,
337         'no_latency_streams': False
338
339     })
340
341 def _get_traffic_client():
342     config = _get_dummy_tg_config('PVP', 'ndr_pdr')
343     config['vxlan'] = False
344     config['mpls'] = 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