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