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