NFVBENCH-156 Add management interface and ssh config in NFVBench image
[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 ChainException
26 from nfvbench.chaining import ChainVnfPort
27 from nfvbench.chaining import InstancePlacer
28 from nfvbench.compute import Compute
29 import nfvbench.credentials
30 from nfvbench.factory import BasicFactory
31 import nfvbench.log
32 from nfvbench.nfvbench import load_default_config
33 from nfvbench.nfvbench import NFVBench
34 from nfvbench.packet_stats import InterfaceStats
35 from nfvbench.specs import ChainType
36 from nfvbench.specs import OpenStackSpec
37 from nfvbench.specs import Specs
38 from nfvbench.summarizer import _annotate_chain_stats
39 from nfvbench.traffic_client import TrafficClient
40 from nfvbench.traffic_gen.traffic_base import Latency
41 from nfvbench.traffic_gen.trex_gen import TRex
42
43
44 # just to get rid of the unused function warning
45 no_op()
46
47
48 def setup_module(module):
49     """Enable log."""
50     nfvbench.log.setup(mute_stdout=False)
51     nfvbench.log.set_level(debug=True)
52
53 def _get_chain_config(sc=ChainType.PVP, scc=1, shared_net=True, rate='1Mpps'):
54     config, _ = load_default_config()
55     config.vm_image_file = 'nfvbenchvm-0.0.qcow2'
56     config.service_chain_count = scc
57     config.service_chain = sc
58     config.service_chain_shared_net = shared_net
59     config.rate = rate
60     config['traffic_generator']['generator_profile'] = [{'name': 'dummy',
61                                                          'tool': 'dummy',
62                                                          'ip': '127.0.0.1',
63                                                          'intf_speed': '10Gbps',
64                                                          'interfaces': [{'port': 0, 'pci': '0.0'},
65                                                                         {'port': 1, 'pci': '0.0'}]}]
66     config.ndr_run = False
67     config.pdr_run = False
68     config.single_run = True
69     config.generator_profile = 'dummy'
70     config.duration_sec = 2
71     config.interval_sec = 1
72     config.openrc_file = "dummy.rc"
73     return config
74
75 def test_chain_runner_ext_no_openstack():
76     """Test ChainRunner EXT no openstack."""
77     config = _get_chain_config(sc=ChainType.EXT)
78     specs = Specs()
79     config.vlans = [100, 200]
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 MagicMock()
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     cred.is_admin = True
124     runner = ChainRunner(config, cred, specs, BasicFactory())
125     runner.close()
126
127 def test_pvp_chain_runner():
128     """Test PVP chain runner."""
129     cred = MagicMock(spec=nfvbench.credentials.Credentials)
130     cred.is_admin = True
131     for shared_net in [True, False]:
132         for sc in [ChainType.PVP]:
133             for scc in [1, 2]:
134                 config = _get_chain_config(sc, scc, shared_net)
135                 _test_pvp_chain(config, cred)
136
137
138 # Test not admin exception with empty value is raised
139 @patch.object(Compute, 'find_image', _mock_find_image)
140 @patch('nfvbench.chaining.Client')
141 @patch('nfvbench.chaining.neutronclient')
142 @patch('nfvbench.chaining.glanceclient')
143 def _test_pvp_chain_no_admin_no_config_values(config, cred, mock_glance, mock_neutron, mock_client):
144     # instance = self.novaclient.servers.create(name=vmname,...)
145     # instance.status == 'ACTIVE'
146     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
147     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
148     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
149     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
150     specs = Specs()
151     openstack_spec = OpenStackSpec()
152     specs.set_openstack_spec(openstack_spec)
153     runner = ChainRunner(config, cred, specs, BasicFactory())
154     runner.close()
155
156 def test_pvp_chain_runner_no_admin_no_config_values():
157     """Test PVP chain runner."""
158     cred = MagicMock(spec=nfvbench.credentials.Credentials)
159     cred.is_admin = False
160     for shared_net in [True, False]:
161         for sc in [ChainType.PVP]:
162             for scc in [1, 2]:
163                 config = _get_chain_config(sc, scc, shared_net)
164                 with pytest.raises(ChainException):
165                     _test_pvp_chain_no_admin_no_config_values(config, cred)
166
167 # Test not admin with mandatory parameters values in config file
168 @patch.object(Compute, 'find_image', _mock_find_image)
169 @patch('nfvbench.chaining.Client')
170 @patch('nfvbench.chaining.neutronclient')
171 @patch('nfvbench.chaining.glanceclient')
172 def _test_pvp_chain_no_admin_config_values(config, cred, mock_glance, mock_neutron, mock_client):
173     # instance = self.novaclient.servers.create(name=vmname,...)
174     # instance.status == 'ACTIVE'
175     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
176     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
177     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
178     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
179     specs = Specs()
180     openstack_spec = OpenStackSpec()
181     specs.set_openstack_spec(openstack_spec)
182     runner = ChainRunner(config, cred, specs, BasicFactory())
183     runner.close()
184
185 def test_pvp_chain_runner_no_admin_config_values():
186     """Test PVP chain runner."""
187     cred = MagicMock(spec=nfvbench.credentials.Credentials)
188     cred.is_admin = False
189     for shared_net in [True, False]:
190         for sc in [ChainType.PVP]:
191             for scc in [1, 2]:
192                 config = _get_chain_config(sc, scc, shared_net)
193                 config.availability_zone = "az"
194                 config.hypervisor_hostname = "server"
195                 # these are the 2 valid forms of vlan ranges
196                 if scc == 1:
197                     config.vlans = [100, 200]
198                 else:
199                     config.vlans = [[port * 100 + index for index in range(scc)]
200                                     for port in range(2)]
201                 _test_pvp_chain_no_admin_config_values(config, cred)
202
203
204 @patch.object(Compute, 'find_image', _mock_find_image)
205 @patch('nfvbench.chaining.Client')
206 @patch('nfvbench.chaining.neutronclient')
207 @patch('nfvbench.chaining.glanceclient')
208 def _test_ext_chain(config, cred, mock_glance, mock_neutron, mock_client):
209     # instance = self.novaclient.servers.create(name=vmname,...)
210     # instance.status == 'ACTIVE'
211     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
212     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
213     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
214     specs = Specs()
215     openstack_spec = OpenStackSpec()
216     specs.set_openstack_spec(openstack_spec)
217     cred = MagicMock(spec=nfvbench.credentials.Credentials)
218     cred.is_admin = True
219     runner = ChainRunner(config, cred, specs, BasicFactory())
220     runner.close()
221
222 def test_ext_chain_runner():
223     """Test openstack+EXT chain runner.
224
225     Test 8 combinations of configs:
226     shared/not shared net x arp/no_arp x scc 1 or 2
227     """
228     cred = MagicMock(spec=nfvbench.credentials.Credentials)
229     cred.is_admin = True
230     for shared_net in [True, False]:
231         for no_arp in [False, True]:
232             for scc in [1, 2]:
233                 config = _get_chain_config(ChainType.EXT, scc, shared_net)
234                 config.no_arp = no_arp
235                 # this time use a tuple of network names
236                 config['external_networks']['left'] = ('ext-lnet00', 'ext-lnet01')
237                 config['external_networks']['right'] = ('ext-rnet00', 'ext-rnet01')
238                 if no_arp:
239                     # If EXT and no arp, the config must provide mac addresses (1 pair per chain)
240                     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00'] * scc
241                     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00'] * scc
242                 _test_ext_chain(config, cred)
243
244 def _check_nfvbench_openstack(sc=ChainType.PVP, l2_loopback=False):
245     for scc in range(1, 3):
246         config = _get_chain_config(sc, scc=scc, shared_net=True)
247         if l2_loopback:
248             config.l2_loopback = True
249             config.vlans = [[100], [200]]
250         if sc == ChainType.EXT:
251             config['external_networks']['left'] = 'ext-lnet'
252             config['external_networks']['right'] = 'ext-rnet'
253         factory = BasicFactory()
254         config_plugin = factory.get_config_plugin_class()(config)
255         config = config_plugin.get_config()
256         openstack_spec = config_plugin.get_openstack_spec()
257         nfvb = NFVBench(config, openstack_spec, config_plugin, factory)
258         res = nfvb.run({}, 'pytest')
259         if res['status'] != 'OK':
260             print res
261         assert res['status'] == 'OK'
262
263
264 mac_seq = 0
265
266 def _mock_get_mac(dummy):
267     global mac_seq
268     mac_seq += 1
269     return '01:00:00:00:00:%02x' % mac_seq
270
271 @patch.object(Compute, 'find_image', _mock_find_image)
272 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
273 @patch.object(ChainVnfPort, 'get_mac', _mock_get_mac)
274 @patch.object(TrafficClient, 'is_udp', lambda x, y: True)
275 @patch('nfvbench.chaining.Client')
276 @patch('nfvbench.chaining.neutronclient')
277 @patch('nfvbench.chaining.glanceclient')
278 @patch('nfvbench.nfvbench.credentials')
279 def test_nfvbench_run(mock_cred, mock_glance, mock_neutron, mock_client):
280     """Test NFVbench class with openstack+PVP."""
281     # instance = self.novaclient.servers.create(name=vmname,...)
282     # instance.status == 'ACTIVE'
283     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
284     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
285     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
286     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
287     _check_nfvbench_openstack()
288
289 @patch.object(Compute, 'find_image', _mock_find_image)
290 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
291 @patch.object(TrafficClient, 'is_udp', lambda x, y: True)
292 @patch('nfvbench.chaining.Client')
293 @patch('nfvbench.chaining.neutronclient')
294 @patch('nfvbench.chaining.glanceclient')
295 @patch('nfvbench.nfvbench.credentials')
296 def test_nfvbench_ext_arp(mock_cred, mock_glance, mock_neutron, mock_client):
297     """Test NFVbench class with openstack+EXT+ARP."""
298     # instance = self.novaclient.servers.create(name=vmname,...)
299     # instance.status == 'ACTIVE'
300     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
301     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
302     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
303     _check_nfvbench_openstack(sc=ChainType.EXT)
304
305 @patch.object(Compute, 'find_image', _mock_find_image)
306 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
307 @patch.object(TrafficClient, 'is_udp', lambda x, y: True)
308 @patch('nfvbench.chaining.Client')
309 @patch('nfvbench.chaining.neutronclient')
310 @patch('nfvbench.chaining.glanceclient')
311 @patch('nfvbench.nfvbench.credentials')
312 def test_nfvbench_l2_loopback(mock_cred, mock_glance, mock_neutron, mock_client):
313     """Test NFVbench class with l2-loopback."""
314     # instance = self.novaclient.servers.create(name=vmname,...)
315     # instance.status == 'ACTIVE'
316     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
317     _check_nfvbench_openstack(l2_loopback=True)
318
319
320 # This is a reduced version of flow stats coming from Trex
321 # with 2 chains and latency for a total of 8 packet groups
322 # Random numbers with random losses
323 CH0_P0_TX = 1234
324 CH0_P1_RX = 1200
325 CH0_P1_TX = 28900
326 CH0_P0_RX = 28000
327 LCH0_P0_TX = 167
328 LCH0_P1_RX = 130
329 LCH0_P1_TX = 523
330 LCH0_P0_RX = 490
331 CH1_P0_TX = 132344
332 CH1_P1_RX = 132004
333 CH1_P1_TX = 1289300
334 CH1_P0_RX = 1280400
335 LCH1_P0_TX = 51367
336 LCH1_P1_RX = 5730
337 LCH1_P1_TX = 35623
338 LCH1_P0_RX = 67
339
340 TREX_STATS = {
341     'flow_stats': {
342         # chain 0 port 0 normal stream
343         0: {'rx_pkts': {0: 0, 1: CH0_P1_RX, 'total': CH0_P1_RX},
344             'tx_pkts': {0: CH0_P0_TX, 1: 0, 'total': CH0_P0_TX}},
345         # chain 1 port 0 normal stream
346         1: {'rx_pkts': {0: 0, 1: CH1_P1_RX, 'total': CH1_P1_RX},
347             'tx_pkts': {0: CH1_P0_TX, 1: 0, 'total': CH1_P0_TX}},
348         # chain 0 port 1 normal stream
349         128: {'rx_pkts': {0: CH0_P0_RX, 1: 0, 'total': CH0_P0_RX},
350               'tx_pkts': {0: 0, 1: CH0_P1_TX, 'total': CH0_P1_TX}},
351         # chain 1 port 1 normal stream
352         129: {'rx_pkts': {0: CH1_P0_RX, 1: 0, 'total': CH1_P0_RX},
353               'tx_pkts': {0: 0, 1: CH1_P1_TX, 'total': CH1_P1_TX}},
354         # chain 0 port 0 latency stream
355         256: {'rx_pkts': {0: 0, 1: LCH0_P1_RX, 'total': LCH0_P1_RX},
356               'tx_pkts': {0: LCH0_P0_TX, 1: 0, 'total': LCH0_P0_TX}},
357         # chain 1 port 0 latency stream
358         257: {'rx_pkts': {0: 0, 1: LCH1_P1_RX, 'total': LCH1_P1_RX},
359               'tx_pkts': {0: LCH1_P0_TX, 1: 0, 'total': LCH1_P0_TX}},
360         # chain 0 port 1 latency stream
361         384: {'rx_pkts': {0: LCH0_P0_RX, 1: 0, 'total': LCH0_P0_RX},
362               'tx_pkts': {0: 0, 1: LCH0_P1_TX, 'total': LCH0_P1_TX}},
363         # chain 1 port 1 latency stream
364         385: {'rx_pkts': {0: LCH1_P0_RX, 1: 0, 'total': LCH1_P0_RX},
365               'tx_pkts': {0: 0, 1: LCH1_P1_TX, 'total': LCH1_P1_TX}}}}
366
367 def test_trex_streams_stats():
368     """Test TRex stats for chains 0 and 1."""
369     traffic_client = MagicMock()
370     trex = TRex(traffic_client)
371     if_stats = [InterfaceStats("p0", "dev0"), InterfaceStats("p1", "dev1")]
372     latencies = [Latency()] * 2
373     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 0)
374     assert if_stats[0].tx == CH0_P0_TX + LCH0_P0_TX
375     assert if_stats[0].rx == CH0_P0_RX + LCH0_P0_RX
376     assert if_stats[1].tx == CH0_P1_TX + LCH0_P1_TX
377     assert if_stats[1].rx == CH0_P1_RX + LCH0_P1_RX
378
379     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 1)
380     assert if_stats[0].tx == CH1_P0_TX + LCH1_P0_TX
381     assert if_stats[0].rx == CH1_P0_RX + LCH1_P0_RX
382     assert if_stats[1].tx == CH1_P1_TX + LCH1_P1_TX
383     assert if_stats[1].rx == CH1_P1_RX + LCH1_P1_RX
384
385 def check_placer(az, hyp, req_az, resolved=False):
386     """Combine multiple combinatoons of placer tests."""
387     placer = InstancePlacer(az, hyp)
388     assert placer.is_resolved() == resolved
389     assert placer.get_required_az() == req_az
390     assert placer.register_full_name('nova:comp1')
391     assert placer.is_resolved()
392     assert placer.get_required_az() == 'nova:comp1'
393
394 def test_placer_no_user_pref():
395     """Test placement when user does not provide any preference."""
396     check_placer(None, None, '')
397
398 def test_placer_user_az():
399     """Test placement when user only provides an az."""
400     check_placer('nova', None, 'nova:')
401     check_placer(None, 'nova:', 'nova:')
402     check_placer('nebula', 'nova:', 'nova:')
403
404 def test_placer_user_hyp():
405     """Test placement when user provides a hypervisor."""
406     check_placer(None, 'comp1', ':comp1')
407     check_placer('nova', 'comp1', 'nova:comp1', resolved=True)
408     check_placer(None, 'nova:comp1', 'nova:comp1', resolved=True)
409     # hyp overrides az
410     check_placer('nebula', 'nova:comp1', 'nova:comp1', resolved=True)
411     # also check for cases of extra parts (more than 1 ':')
412     check_placer('nova:nebula', 'comp1', 'nova:comp1', resolved=True)
413
414
415 def test_placer_negative():
416     """Run negative tests on placer."""
417     # AZ mismatch
418     with pytest.raises(Exception):
419         placer = InstancePlacer('nova', None)
420         placer.register('nebula:comp1')
421     # comp mismatch
422     with pytest.raises(Exception):
423         placer = InstancePlacer(None, 'comp1')
424         placer.register('nebula:comp2')
425
426
427 # without total, with total and only 2 col
428 CHAIN_STATS = [{0: {'packets': [2000054, 1999996, 1999996]}},
429                {0: {'packets': [2000054, 1999996, 1999996]},
430                 1: {'packets': [2000054, 2000054, 2000054]},
431                 'total': {'packets': [4000108, 4000050, 4000050]}},
432                {0: {'packets': [2000054, 2000054]}},
433                {0: {'packets': [2000054, 1999996]}},
434                # shared networks no drops, shared nets will have empty strings
435                {0: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
436                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
437                 'total': {'packets': [30000004, 30000004, 30000004, 30000004, 30000004, 30000004]}},
438                {0: {'packets': [15000002, '', 14000002, 14000002, '', 13000002]},
439                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
440                 'total': {'packets': [30000004, 29000004, 29000004, 29000004, 29000004, 28000004]}},
441                # example with non-available rx count in last position
442                {0: {'packets': [2000054, 1999996, None]},
443                 1: {'packets': [2000054, 2000054, None]},
444                 'total': {'packets': [4000108, 4000050, 4000050]}}]
445 XP_CHAIN_STATS = [{0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]}},
446                   {0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]},
447                    1: {'packets': [2000054, '=>', 2000054]},
448                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}},
449                   {0: {'packets': [2000054, 2000054]}},
450                   {0: {'packets': [2000054, '-58 (-0.0029%)']}},
451                   # shared net, leave spaces alone
452                   {0: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
453                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
454                    'total': {'packets': [30000004, '=>', '=>', '=>', '=>', 30000004]}},
455                   {0: {'packets': [15000002, '', '-1,000,000 (-6.6667%)', '=>', '',
456                                    '-1,000,000 (-7.1429%)']},
457                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
458                    'total': {'packets': [30000004, '-1,000,000 (-3.3333%)', '=>', '=>', '=>',
459                                          '-1,000,000 (-3.4483%)']}},
460                   {0: {'packets': [2000054, '-58 (-0.0029%)', 'n/a']},
461                    1: {'packets': [2000054, '=>', 'n/a']},
462                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}}]
463
464
465 def test_summarizer():
466     """Test Summarizer class."""
467     for stats, exp_stats in zip(CHAIN_STATS, XP_CHAIN_STATS):
468         _annotate_chain_stats(stats)
469         assert stats == exp_stats
470
471 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
472 @patch.object(TrafficClient, 'is_udp', lambda x, y: True)
473 def test_fixed_rate_no_openstack():
474     """Test FIxed Rate run - no openstack."""
475     config = _get_chain_config(ChainType.EXT, 1, True, rate='100%')
476     specs = Specs()
477     config.vlans = [100, 200]
478     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
479     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
480     config.no_arp = True
481     config['vlan_tagging'] = True
482     config['traffic'] = {'profile': 'profile_64',
483                          'bidirectional': True}
484     config['traffic_profile'] = [{'name': 'profile_64', 'l2frame_size': ['64']}]
485
486     runner = ChainRunner(config, None, specs, BasicFactory())
487     tg = runner.traffic_client.gen
488
489     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=50, max_11_tx=50)
490     # tx packets should be 50% at requested 50% line rate or higher for 64B and no drops...
491     results = runner.run()
492     assert results
493     # pprint.pprint(results['EXT']['result']['result']['64'])
494     runner.close()