5490dfca598bf191a31058ae9c94e78fccefc407
[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('nfvbench.chaining.Client')
275 @patch('nfvbench.chaining.neutronclient')
276 @patch('nfvbench.chaining.glanceclient')
277 @patch('nfvbench.nfvbench.credentials')
278 def test_nfvbench_run(mock_cred, mock_glance, mock_neutron, mock_client):
279     """Test NFVbench class with openstack+PVP."""
280     # instance = self.novaclient.servers.create(name=vmname,...)
281     # instance.status == 'ACTIVE'
282     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
283     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
284     mock_neutron.Client.return_value.create_network.return_value = {'network': netw}
285     mock_neutron.Client.return_value.list_networks.return_value = {'networks': None}
286     _check_nfvbench_openstack()
287
288 @patch.object(Compute, 'find_image', _mock_find_image)
289 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
290 @patch('nfvbench.chaining.Client')
291 @patch('nfvbench.chaining.neutronclient')
292 @patch('nfvbench.chaining.glanceclient')
293 @patch('nfvbench.nfvbench.credentials')
294 def test_nfvbench_ext_arp(mock_cred, mock_glance, mock_neutron, mock_client):
295     """Test NFVbench class with openstack+EXT+ARP."""
296     # instance = self.novaclient.servers.create(name=vmname,...)
297     # instance.status == 'ACTIVE'
298     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
299     netw = {'id': 0, 'provider:network_type': 'vlan', 'provider:segmentation_id': 1000}
300     mock_neutron.Client.return_value.list_networks.return_value = {'networks': [netw]}
301     _check_nfvbench_openstack(sc=ChainType.EXT)
302
303 @patch.object(Compute, 'find_image', _mock_find_image)
304 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
305 @patch('nfvbench.chaining.Client')
306 @patch('nfvbench.chaining.neutronclient')
307 @patch('nfvbench.chaining.glanceclient')
308 @patch('nfvbench.nfvbench.credentials')
309 def test_nfvbench_l2_loopback(mock_cred, mock_glance, mock_neutron, mock_client):
310     """Test NFVbench class with l2-loopback."""
311     # instance = self.novaclient.servers.create(name=vmname,...)
312     # instance.status == 'ACTIVE'
313     mock_client.return_value.servers.create.return_value.status = 'ACTIVE'
314     _check_nfvbench_openstack(l2_loopback=True)
315
316
317 # This is a reduced version of flow stats coming from Trex
318 # with 2 chains and latency for a total of 8 packet groups
319 # Random numbers with random losses
320 CH0_P0_TX = 1234
321 CH0_P1_RX = 1200
322 CH0_P1_TX = 28900
323 CH0_P0_RX = 28000
324 LCH0_P0_TX = 167
325 LCH0_P1_RX = 130
326 LCH0_P1_TX = 523
327 LCH0_P0_RX = 490
328 CH1_P0_TX = 132344
329 CH1_P1_RX = 132004
330 CH1_P1_TX = 1289300
331 CH1_P0_RX = 1280400
332 LCH1_P0_TX = 51367
333 LCH1_P1_RX = 5730
334 LCH1_P1_TX = 35623
335 LCH1_P0_RX = 67
336
337 TREX_STATS = {
338     'flow_stats': {
339         # chain 0 port 0 normal stream
340         0: {'rx_pkts': {0: 0, 1: CH0_P1_RX, 'total': CH0_P1_RX},
341             'tx_pkts': {0: CH0_P0_TX, 1: 0, 'total': CH0_P0_TX}},
342         # chain 1 port 0 normal stream
343         1: {'rx_pkts': {0: 0, 1: CH1_P1_RX, 'total': CH1_P1_RX},
344             'tx_pkts': {0: CH1_P0_TX, 1: 0, 'total': CH1_P0_TX}},
345         # chain 0 port 1 normal stream
346         128: {'rx_pkts': {0: CH0_P0_RX, 1: 0, 'total': CH0_P0_RX},
347               'tx_pkts': {0: 0, 1: CH0_P1_TX, 'total': CH0_P1_TX}},
348         # chain 1 port 1 normal stream
349         129: {'rx_pkts': {0: CH1_P0_RX, 1: 0, 'total': CH1_P0_RX},
350               'tx_pkts': {0: 0, 1: CH1_P1_TX, 'total': CH1_P1_TX}},
351         # chain 0 port 0 latency stream
352         256: {'rx_pkts': {0: 0, 1: LCH0_P1_RX, 'total': LCH0_P1_RX},
353               'tx_pkts': {0: LCH0_P0_TX, 1: 0, 'total': LCH0_P0_TX}},
354         # chain 1 port 0 latency stream
355         257: {'rx_pkts': {0: 0, 1: LCH1_P1_RX, 'total': LCH1_P1_RX},
356               'tx_pkts': {0: LCH1_P0_TX, 1: 0, 'total': LCH1_P0_TX}},
357         # chain 0 port 1 latency stream
358         384: {'rx_pkts': {0: LCH0_P0_RX, 1: 0, 'total': LCH0_P0_RX},
359               'tx_pkts': {0: 0, 1: LCH0_P1_TX, 'total': LCH0_P1_TX}},
360         # chain 1 port 1 latency stream
361         385: {'rx_pkts': {0: LCH1_P0_RX, 1: 0, 'total': LCH1_P0_RX},
362               'tx_pkts': {0: 0, 1: LCH1_P1_TX, 'total': LCH1_P1_TX}}}}
363
364 def test_trex_streams_stats():
365     """Test TRex stats for chains 0 and 1."""
366     traffic_client = MagicMock()
367     trex = TRex(traffic_client)
368     if_stats = [InterfaceStats("p0", "dev0"), InterfaceStats("p1", "dev1")]
369     latencies = [Latency()] * 2
370     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 0)
371     assert if_stats[0].tx == CH0_P0_TX + LCH0_P0_TX
372     assert if_stats[0].rx == CH0_P0_RX + LCH0_P0_RX
373     assert if_stats[1].tx == CH0_P1_TX + LCH0_P1_TX
374     assert if_stats[1].rx == CH0_P1_RX + LCH0_P1_RX
375
376     trex.get_stream_stats(TREX_STATS, if_stats, latencies, 1)
377     assert if_stats[0].tx == CH1_P0_TX + LCH1_P0_TX
378     assert if_stats[0].rx == CH1_P0_RX + LCH1_P0_RX
379     assert if_stats[1].tx == CH1_P1_TX + LCH1_P1_TX
380     assert if_stats[1].rx == CH1_P1_RX + LCH1_P1_RX
381
382 def check_placer(az, hyp, req_az, resolved=False):
383     """Combine multiple combinatoons of placer tests."""
384     placer = InstancePlacer(az, hyp)
385     assert placer.is_resolved() == resolved
386     assert placer.get_required_az() == req_az
387     assert placer.register_full_name('nova:comp1')
388     assert placer.is_resolved()
389     assert placer.get_required_az() == 'nova:comp1'
390
391 def test_placer_no_user_pref():
392     """Test placement when user does not provide any preference."""
393     check_placer(None, None, '')
394
395 def test_placer_user_az():
396     """Test placement when user only provides an az."""
397     check_placer('nova', None, 'nova:')
398     check_placer(None, 'nova:', 'nova:')
399     check_placer('nebula', 'nova:', 'nova:')
400
401 def test_placer_user_hyp():
402     """Test placement when user provides a hypervisor."""
403     check_placer(None, 'comp1', ':comp1')
404     check_placer('nova', 'comp1', 'nova:comp1', resolved=True)
405     check_placer(None, 'nova:comp1', 'nova:comp1', resolved=True)
406     # hyp overrides az
407     check_placer('nebula', 'nova:comp1', 'nova:comp1', resolved=True)
408     # also check for cases of extra parts (more than 1 ':')
409     check_placer('nova:nebula', 'comp1', 'nova:comp1', resolved=True)
410
411
412 def test_placer_negative():
413     """Run negative tests on placer."""
414     # AZ mismatch
415     with pytest.raises(Exception):
416         placer = InstancePlacer('nova', None)
417         placer.register('nebula:comp1')
418     # comp mismatch
419     with pytest.raises(Exception):
420         placer = InstancePlacer(None, 'comp1')
421         placer.register('nebula:comp2')
422
423
424 # without total, with total and only 2 col
425 CHAIN_STATS = [{0: {'packets': [2000054, 1999996, 1999996]}},
426                {0: {'packets': [2000054, 1999996, 1999996]},
427                 1: {'packets': [2000054, 2000054, 2000054]},
428                 'total': {'packets': [4000108, 4000050, 4000050]}},
429                {0: {'packets': [2000054, 2000054]}},
430                {0: {'packets': [2000054, 1999996]}},
431                # shared networks no drops, shared nets will have empty strings
432                {0: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
433                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
434                 'total': {'packets': [30000004, 30000004, 30000004, 30000004, 30000004, 30000004]}},
435                {0: {'packets': [15000002, '', 14000002, 14000002, '', 13000002]},
436                 1: {'packets': [15000002, '', 15000002, 15000002, '', 15000002]},
437                 'total': {'packets': [30000004, 29000004, 29000004, 29000004, 29000004, 28000004]}},
438                # example with non-available rx count in last position
439                {0: {'packets': [2000054, 1999996, None]},
440                 1: {'packets': [2000054, 2000054, None]},
441                 'total': {'packets': [4000108, 4000050, 4000050]}}]
442 XP_CHAIN_STATS = [{0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]}},
443                   {0: {'packets': [2000054, '-58 (-0.0029%)', 1999996]},
444                    1: {'packets': [2000054, '=>', 2000054]},
445                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}},
446                   {0: {'packets': [2000054, 2000054]}},
447                   {0: {'packets': [2000054, '-58 (-0.0029%)']}},
448                   # shared net, leave spaces alone
449                   {0: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
450                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
451                    'total': {'packets': [30000004, '=>', '=>', '=>', '=>', 30000004]}},
452                   {0: {'packets': [15000002, '', '-1,000,000 (-6.6667%)', '=>', '',
453                                    '-1,000,000 (-7.1429%)']},
454                    1: {'packets': [15000002, '', '=>', '=>', '', 15000002]},
455                    'total': {'packets': [30000004, '-1,000,000 (-3.3333%)', '=>', '=>', '=>',
456                                          '-1,000,000 (-3.4483%)']}},
457                   {0: {'packets': [2000054, '-58 (-0.0029%)', 'n/a']},
458                    1: {'packets': [2000054, '=>', 'n/a']},
459                    'total': {'packets': [4000108, '-58 (-0.0014%)', 4000050]}}]
460
461
462 def test_summarizer():
463     """Test Summarizer class."""
464     for stats, exp_stats in zip(CHAIN_STATS, XP_CHAIN_STATS):
465         _annotate_chain_stats(stats)
466         assert stats == exp_stats
467
468 @patch.object(TrafficClient, 'skip_sleep', lambda x: True)
469 def test_fixed_rate_no_openstack():
470     """Test FIxed Rate run - no openstack."""
471     config = _get_chain_config(ChainType.EXT, 1, True, rate='100%')
472     specs = Specs()
473     config.vlans = [100, 200]
474     config['traffic_generator']['mac_addrs_left'] = ['00:00:00:00:00:00']
475     config['traffic_generator']['mac_addrs_right'] = ['00:00:00:00:01:00']
476     config.no_arp = True
477     config['vlan_tagging'] = True
478     config['traffic'] = {'profile': 'profile_64',
479                          'bidirectional': True}
480     config['traffic_profile'] = [{'name': 'profile_64', 'l2frame_size': ['64']}]
481
482     runner = ChainRunner(config, None, specs, BasicFactory())
483     tg = runner.traffic_client.gen
484
485     tg.set_response_curve(lr_dr=0, ndr=100, max_actual_tx=50, max_11_tx=50)
486     # tx packets should be 50% at requested 50% line rate or higher for 64B and no drops...
487     results = runner.run()
488     assert results
489     # pprint.pprint(results['EXT']['result']['result']['64'])
490     runner.close()