NFVBENCH-111 Add support for VxLAN
[nfvbench.git] / test / test_chains.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 """Test Chaining functions."""
17
18 from mock_trex import no_op
19
20 from mock import MagicMock
21 from mock import patch
22 import pytest
23
24 from nfvbench.chain_runner import ChainRunner
25 from nfvbench.chaining import ChainVnfPort
26 from nfvbench.chaining import InstancePlacer
27 from nfvbench.compute import Compute
28 import nfvbench.credentials
29 from nfvbench.factory import BasicFactory
30 import nfvbench.log
31 from nfvbench.nfvbench import load_default_config
32 from nfvbench.nfvbench import NFVBench
33 from nfvbench.packet_stats import InterfaceStats
34 from nfvbench.specs import ChainType
35 from nfvbench.specs import OpenStackSpec
36 from nfvbench.specs import Specs
37 from nfvbench.summarizer import _annotate_chain_stats
38 from nfvbench.traffic_client import TrafficClient
39 from nfvbench.traffic_gen.traffic_base import Latency
40 from nfvbench.traffic_gen.trex import TRex
41
42
43 # just to get rid of the unused function warning
44 no_op()
45
46
47 def setup_module(module):
48     """Enable log."""
49     nfvbench.log.setup(mute_stdout=False)
50     nfvbench.log.set_level(debug=True)
51
52 def _get_chain_config(sc=ChainType.PVP, scc=1, shared_net=True):
53     config, _ = load_default_config()
54     config.vm_image_file = 'nfvbenchvm-0.0.qcow2'
55     config.service_chain_count = scc
56     config.service_chain = sc
57     config.service_chain_shared_net = shared_net
58     config.rate = '1Mpps'
59     config['traffic_generator']['generator_profile'] = [{'name': 'dummy',
60                                                          'tool': 'dummy',
61                                                          'ip': '127.0.0.1',
62                                                          'intf_speed': None,
63                                                          'interfaces': [{'port': 0, 'pci': '0.0'},
64                                                                         {'port': 1, 'pci': '0.0'}]}]
65     config.ndr_run = False
66     config.pdr_run = False
67     config.single_run = True
68     config.generator_profile = 'dummy'
69     config.duration_sec = 2
70     config.interval_sec = 1
71     config.openrc_file = "dummy.rc"
72     return config
73
74 def test_chain_runner_ext_no_openstack():
75     """Test ChainRunner EXT no openstack."""
76     config = _get_chain_config(sc=ChainType.EXT)
77     specs = Specs()
78     config.vlans = [100, 200]
79     config.vnis = [5000, 6000]
80     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
81     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
82
83     for shared_net in [True, False]:
84         for no_arp in [False, True]:
85             for vlan_tag in [False, True]:
86                 for scc in [1, 2]:
87                     config = _get_chain_config(ChainType.EXT, scc, shared_net)
88                     config.no_arp = no_arp
89                     if no_arp:
90                         # If EXT and no arp, the config must provide mac (1 pair per chain)
91                         config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00'] * scc
92                         config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00'] * scc
93                     config['vlan_tagging'] = vlan_tag
94                     if vlan_tag:
95                         # these are the 2 valid forms of vlan ranges
96                         if scc == 1:
97                             config.vlans = [100, 200]
98                         else:
99                             config.vlans = [[port * 100 + index for index in range(scc)]
100                                             for port in range(2)]
101                     runner = ChainRunner(config, None, specs, BasicFactory())
102                     runner.close()
103
104
105 def _mock_find_image(self, image_name):
106     return True
107
108 @patch.object(Compute, 'find_image', _mock_find_image)
109 @patch('nfvbench.chaining.Client')
110 @patch('nfvbench.chaining.neutronclient')
111 @patch('nfvbench.chaining.glanceclient')
112 def _test_pvp_chain(config, cred, mock_glance, mock_neutron, mock_client):
113     # instance = self.novaclient.servers.create(name=vmname,...)
114     # instance.status == 'ACTIVE'
115     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
116     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
117     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
118     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
119     specs = Specs()
120     openstack_spec = OpenStackSpec()
121     specs.set_openstack_spec(openstack_spec)
122     cred = MagicMock(spec=nfvbench.credentials.Credentials)
123     runner = ChainRunner(config, cred, specs, BasicFactory())
124     runner.close()
125
126 def test_pvp_chain_runner():
127     """Test PVP chain runner."""
128     cred = MagicMock(spec=nfvbench.credentials.Credentials)
129     for shared_net in [True, False]:
130         for sc in [ChainType.PVP]:
131             for scc in [1, 2]:
132                 config = _get_chain_config(sc, scc, shared_net)
133                 _test_pvp_chain(config, cred)
134
135 @patch.object(Compute, 'find_image', _mock_find_image)
136 @patch('nfvbench.chaining.Client')
137 @patch('nfvbench.chaining.neutronclient')
138 @patch('nfvbench.chaining.glanceclient')
139 def _test_ext_chain(config, cred, mock_glance, mock_neutron, mock_client):
140     # instance = self.novaclient.servers.create(name=vmname,...)
141     # instance.status == 'ACTIVE'
142     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
143     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
144     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
145     specs = Specs()
146     openstack_spec = OpenStackSpec()
147     specs.set_openstack_spec(openstack_spec)
148     cred = MagicMock(spec=nfvbench.credentials.Credentials)
149     runner = ChainRunner(config, cred, specs, BasicFactory())
150     runner.close()
151
152 def test_ext_chain_runner():
153     """Test openstack+EXT chain runner.
154
155     Test 8 combinations of configs:
156     shared/not shared net x arp/no_arp x scc 1 or 2
157     """
158     cred = MagicMock(spec=nfvbench.credentials.Credentials)
159     for shared_net in [True, False]:
160         for no_arp in [False, True]:
161             for scc in [1, 2]:
162                 config = _get_chain_config(ChainType.EXT, scc, shared_net)
163                 config.no_arp = no_arp
164                 if no_arp:
165                     # If EXT and no arp, the config must provide mac addresses (1 pair per chain)
166                     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00'] * scc
167                     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00'] * scc
168                 _test_ext_chain(config, cred)
169
170 def _check_nfvbench_openstack(sc=ChainType.PVP, l2_loopback=False):
171     for scc in range(1, 3):
172         config = _get_chain_config(sc, scc=scc, shared_net=True)
173         if l2_loopback:
174             config.l2_loopback = True
175             config.vlans = [[100], [200]]
176             config.vnis = [[5000], [6000]]
177         factory = BasicFactory()
178         config_plugin = factory.get_config_plugin_class()(config)
179         config = config_plugin.get_config()
180         openstack_spec = config_plugin.get_openstack_spec()
181         nfvb = NFVBench(config, openstack_spec, config_plugin, factory)
182         res = nfvb.run({}, 'pytest')
183         if res['status'] != 'OK':
184             print res
185         assert res['status'] == 'OK'
186
187
188 mac_seq = 0
189
190 def _mock_get_mac(dummy):
191     global mac_seq
192     mac_seq += 1
193     return '01:00:00:00:00:%02x' % mac_seq
194
195 @patch.object(Compute, 'find_image', _mock_find_image)
196 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
197 @patch.object(ChainVnfPort, 'get_mac', _mock_get_mac)
198 @patch('nfvbench.chaining.Client')
199 @patch('nfvbench.chaining.neutronclient')
200 @patch('nfvbench.chaining.glanceclient')
201 @patch('nfvbench.nfvbench.credentials')
202 def test_nfvbench_run(mock_cred, mock_glance, mock_neutron, mock_client):
203     """Test NFVbench class with openstack+PVP."""
204     # instance = self.novaclient.servers.create(name=vmname,...)
205     # instance.status == 'ACTIVE'
206     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
207     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
208     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
209     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
210     _check_nfvbench_openstack()
211
212 @patch.object(Compute, 'find_image', _mock_find_image)
213 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
214 @patch('nfvbench.chaining.Client')
215 @patch('nfvbench.chaining.neutronclient')
216 @patch('nfvbench.chaining.glanceclient')
217 @patch('nfvbench.nfvbench.credentials')
218 def test_nfvbench_ext_arp(mock_cred, mock_glance, mock_neutron, mock_client):
219     """Test NFVbench class with openstack+EXT+ARP."""
220     # instance = self.novaclient.servers.create(name=vmname,...)
221     # instance.status == 'ACTIVE'
222     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
223     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
224     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
225     _check_nfvbench_openstack(sc=ChainType.EXT)
226
227 @patch.object(Compute, 'find_image', _mock_find_image)
228 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
229 @patch('nfvbench.chaining.Client')
230 @patch('nfvbench.chaining.neutronclient')
231 @patch('nfvbench.chaining.glanceclient')
232 @patch('nfvbench.nfvbench.credentials')
233 def test_nfvbench_l2_loopback(mock_cred, mock_glance, mock_neutron, mock_client):
234     """Test NFVbench class with l2-loopback."""
235     # instance = self.novaclient.servers.create(name=vmname,...)
236     # instance.status == 'ACTIVE'
237     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
238     _check_nfvbench_openstack(l2_loopback=True)
239
240
241 # This is a reduced version of flow stats coming from Trex
242 # with 2 chains and latency for a total of 8 packet groups
243 # Random numbers with random losses
244 CH0_P0_TX = 1234
245 CH0_P1_RX = 1200
246 CH0_P1_TX = 28900
247 CH0_P0_RX = 28000
248 LCH0_P0_TX = 167
249 LCH0_P1_RX = 130
250 LCH0_P1_TX = 523
251 LCH0_P0_RX = 490
252 CH1_P0_TX = 132344
253 CH1_P1_RX = 132004
254 CH1_P1_TX = 1289300
255 CH1_P0_RX = 1280400
256 LCH1_P0_TX = 51367
257 LCH1_P1_RX = 5730
258 LCH1_P1_TX = 35623
259 LCH1_P0_RX = 67
260
261 TREX_STATS = {
262     'flow_stats': {
263         # chain 0 port 0 normal stream
264         0: {'rx_pkts': {0: 0, 1: CH0_P1_RX, 'total': CH0_P1_RX},
265             'tx_pkts': {0: CH0_P0_TX, 1: 0, 'total': CH0_P0_TX}},
266         # chain 1 port 0 normal stream
267         1: {'rx_pkts': {0: 0, 1: CH1_P1_RX, 'total': CH1_P1_RX},
268             'tx_pkts': {0: CH1_P0_TX, 1: 0, 'total': CH1_P0_TX}},
269         # chain 0 port 1 normal stream
270         128: {'rx_pkts': {0: CH0_P0_RX, 1: 0, 'total': CH0_P0_RX},
271               'tx_pkts': {0: 0, 1: CH0_P1_TX, 'total': CH0_P1_TX}},
272         # chain 1 port 1 normal stream
273         129: {'rx_pkts': {0: CH1_P0_RX, 1: 0, 'total': CH1_P0_RX},
274               'tx_pkts': {0: 0, 1: CH1_P1_TX, 'total': CH1_P1_TX}},
275         # chain 0 port 0 latency stream
276         256: {'rx_pkts': {0: 0, 1: LCH0_P1_RX, 'total': LCH0_P1_RX},
277               'tx_pkts': {0: LCH0_P0_TX, 1: 0, 'total': LCH0_P0_TX}},
278         # chain 1 port 0 latency stream
279         257: {'rx_pkts': {0: 0, 1: LCH1_P1_RX, 'total': LCH1_P1_RX},
280               'tx_pkts': {0: LCH1_P0_TX, 1: 0, 'total': LCH1_P0_TX}},
281         # chain 0 port 1 latency stream
282         384: {'rx_pkts': {0: LCH0_P0_RX, 1: 0, 'total': LCH0_P0_RX},
283               'tx_pkts': {0: 0, 1: LCH0_P1_TX, 'total': LCH0_P1_TX}},
284         # chain 1 port 1 latency stream
285         385: {'rx_pkts': {0: LCH1_P0_RX, 1: 0, 'total': LCH1_P0_RX},
286               'tx_pkts': {0: 0, 1: LCH1_P1_TX, 'total': LCH1_P1_TX}}}}
287
288 def test_trex_streams_stats():
289     """Test TRex stats for chains 0 and 1."""
290     traffic_client = MagicMock()
291     trex = TRex(traffic_client)
292     if_stats = [InterfaceStats("p0", "dev0"), InterfaceStats("p1", "dev1")]
293     latencies = [Latency()] * 2
294     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 0)
295     assert if_stats[0].tx == CH0_P0_TX + LCH0_P0_TX
296     assert if_stats[0].rx == CH0_P0_RX + LCH0_P0_RX
297     assert if_stats[1].tx == CH0_P1_TX + LCH0_P1_TX
298     assert if_stats[1].rx == CH0_P1_RX + LCH0_P1_RX
299
300     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 1)
301     assert if_stats[0].tx == CH1_P0_TX + LCH1_P0_TX
302     assert if_stats[0].rx == CH1_P0_RX + LCH1_P0_RX
303     assert if_stats[1].tx == CH1_P1_TX + LCH1_P1_TX
304     assert if_stats[1].rx == CH1_P1_RX + LCH1_P1_RX
305
306 def check_placer(az, hyp, req_az, resolved=False):
307     """Combine multiple combinatoons of placer tests."""
308     placer = InstancePlacer(az, hyp)
309     assert placer.is_resolved() == resolved
310     assert placer.get_required_az() == req_az
311     assert placer.register_full_name('nova:comp1')
312     assert placer.is_resolved()
313     assert placer.get_required_az() == 'nova:comp1'
314
315 def test_placer_no_user_pref():
316     """Test placement when user does not provide any preference."""
317     check_placer(None, None, '')
318
319 def test_placer_user_az():
320     """Test placement when user only provides an az."""
321     check_placer('nova', None, 'nova:')
322     check_placer(None, 'nova:', 'nova:')
323     check_placer('nebula', 'nova:', 'nova:')
324
325 def test_placer_user_hyp():
326     """Test placement when user provides a hypervisor."""
327     check_placer(None, 'comp1', ':comp1')
328     check_placer('nova', 'comp1', 'nova:comp1', resolved=True)
329     check_placer(None, 'nova:comp1', 'nova:comp1', resolved=True)
330     # hyp overrides az
331     check_placer('nebula', 'nova:comp1', 'nova:comp1', resolved=True)
332     # also check for cases of extra parts (more than 1 ':')
333     check_placer('nova:nebula', 'comp1', 'nova:comp1', resolved=True)
334
335
336 def test_placer_negative():
337     """Run negative tests on placer."""
338     # AZ mismatch
339     with pytest.raises(Exception):
340         placer = InstancePlacer('nova', None)
341         placer.register('nebula:comp1')
342     # comp mismatch
343     with pytest.raises(Exception):
344         placer = InstancePlacer(None, 'comp1')
345         placer.register('nebula:comp2')
346
347
348 # without total, with total and only 2 col
349 CHAIN_STATS = [{0: {'packets': [2000054, 1999996, 1999996]}},
350                {0: {'packets': [2000054, 1999996, 1999996]},
351                 1: {'packets': [2000054, 2000054, 2000054]},
352                 'total': {'packets': [4000108, 4000050, 4000050]}},
353                {0: {'packets': [2000054, 2000054]}},
354                {0: {'packets': [2000054, 1999996]}},
355                # shared networks no drops, shared nets will have empty strings
356                {0: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
357                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
358                 'total': {'packets': [30000004, 30000004, 30000004, 30000004, 30000004, 30000004]}},
359                {0: {'packets': [15000002, '', 14000002, 14000002, '', 13000002]},
360                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
361                 'total': {'packets': [30000004, 29000004, 29000004, 29000004, 29000004, 28000004]}}]
362 XP_CHAIN_STATS = [{0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]}},
363                   {0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]},
364                    1: {'packets': [2000054, '=>', 2000054]},
365                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}},
366                   {0: {'packets': [2000054, 2000054]}},
367                   {0: {'packets': [2000054, '-58 (-0.0029%)']}},
368                   # shared net, leave spaces alone
369                   {0: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
370                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
371                    'total': {'packets': [30000004, '=>', '=>', '=>', '=>', 30000004]}},
372                   {0: {'packets': [15000002, '', '-1,000,000 (-6.6667%)', '=>', '',
373                                    '-1,000,000 (-7.1429%)']},
374                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
375                    'total': {'packets': [30000004, '-1,000,000 (-3.3333%)', '=>', '=>', '=>',
376                                          '-1,000,000 (-3.4483%)']}}]
377
378
379 def test_summarizer():
380     """Test Summarizer class."""
381     for stats, exp_stats in zip(CHAIN_STATS, XP_CHAIN_STATS):
382         _annotate_chain_stats(stats)
383         assert stats == exp_stats