Add possibility to restart TRex in case of config change or forced it with config...
[nfvbench.git] / nfvbench / traffic_gen / trex_gen.py
1 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
2 #
3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
4 #    not use this file except in compliance with the License. You may obtain
5 #    a copy of the License at
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #    Unless required by applicable law or agreed to in writing, software
10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 #    License for the specific language governing permissions and limitations
13 #    under the License.
14 """Driver module for TRex traffic generator."""
15
16 import math
17 import os
18 import random
19 import time
20 import traceback
21
22 from itertools import count
23 from nfvbench.log import LOG
24 from nfvbench.traffic_server import TRexTrafficServer
25 from nfvbench.utils import cast_integer
26 from nfvbench.utils import timeout
27 from nfvbench.utils import TimeoutError
28 from traffic_base import AbstractTrafficGenerator
29 from traffic_base import TrafficGeneratorException
30 import traffic_utils as utils
31 from traffic_utils import IMIX_AVG_L2_FRAME_SIZE
32 from traffic_utils import IMIX_L2_SIZES
33 from traffic_utils import IMIX_RATIOS
34
35 # pylint: disable=import-error
36 from trex.common.services.trex_service_arp import ServiceARP
37 from trex.stl.api import bind_layers
38 from trex.stl.api import CTRexVmInsFixHwCs
39 from trex.stl.api import Dot1Q
40 from trex.stl.api import Ether
41 from trex.stl.api import FlagsField
42 from trex.stl.api import IP
43 from trex.stl.api import Packet
44 from trex.stl.api import STLClient
45 from trex.stl.api import STLError
46 from trex.stl.api import STLFlowLatencyStats
47 from trex.stl.api import STLFlowStats
48 from trex.stl.api import STLPktBuilder
49 from trex.stl.api import STLScVmRaw
50 from trex.stl.api import STLStream
51 from trex.stl.api import STLTXCont
52 from trex.stl.api import STLVmFixChecksumHw
53 from trex.stl.api import STLVmFlowVar
54 from trex.stl.api import STLVmFlowVarRepeatableRandom
55 from trex.stl.api import STLVmWrFlowVar
56 from trex.stl.api import ThreeBytesField
57 from trex.stl.api import UDP
58 from trex.stl.api import XByteField
59
60
61 # pylint: enable=import-error
62
63 class VXLAN(Packet):
64     """VxLAN class."""
65
66     _VXLAN_FLAGS = ['R' * 27] + ['I'] + ['R' * 5]
67     name = "VXLAN"
68     fields_desc = [FlagsField("flags", 0x08000000, 32, _VXLAN_FLAGS),
69                    ThreeBytesField("vni", 0),
70                    XByteField("reserved", 0x00)]
71
72     def mysummary(self):
73         """Summary."""
74         return self.sprintf("VXLAN (vni=%VXLAN.vni%)")
75
76 class TRex(AbstractTrafficGenerator):
77     """TRex traffic generator driver."""
78
79     LATENCY_PPS = 1000
80     CHAIN_PG_ID_MASK = 0x007F
81     PORT_PG_ID_MASK = 0x0080
82     LATENCY_PG_ID_MASK = 0x0100
83
84     def __init__(self, traffic_client):
85         """Trex driver."""
86         AbstractTrafficGenerator.__init__(self, traffic_client)
87         self.client = None
88         self.id = count()
89         self.port_handle = []
90         self.chain_count = self.generator_config.service_chain_count
91         self.rates = []
92         self.capture_id = None
93         self.packet_list = []
94
95     def get_version(self):
96         """Get the Trex version."""
97         return self.client.get_server_version() if self.client else ''
98
99     def get_pg_id(self, port, chain_id):
100         """Calculate the packet group IDs to use for a given port/stream type/chain_id.
101
102         port: 0 or 1
103         chain_id: identifies to which chain the pg_id is associated (0 to 255)
104         return: pg_id, lat_pg_id
105
106         We use a bit mask to set up the 3 fields:
107         0x007F: chain ID (8 bits for a max of 128 chains)
108         0x0080: port bit
109         0x0100: latency bit
110         """
111         pg_id = port * TRex.PORT_PG_ID_MASK | chain_id
112         return pg_id, pg_id | TRex.LATENCY_PG_ID_MASK
113
114     def extract_stats(self, in_stats):
115         """Extract stats from dict returned by Trex API.
116
117         :param in_stats: dict as returned by TRex api
118         """
119         utils.nan_replace(in_stats)
120         # LOG.debug(in_stats)
121
122         result = {}
123         # port_handles should have only 2 elements: [0, 1]
124         # so (1 - ph) will be the index for the far end port
125         for ph in self.port_handle:
126             stats = in_stats[ph]
127             far_end_stats = in_stats[1 - ph]
128             result[ph] = {
129                 'tx': {
130                     'total_pkts': cast_integer(stats['opackets']),
131                     'total_pkt_bytes': cast_integer(stats['obytes']),
132                     'pkt_rate': cast_integer(stats['tx_pps']),
133                     'pkt_bit_rate': cast_integer(stats['tx_bps'])
134                 },
135                 'rx': {
136                     'total_pkts': cast_integer(stats['ipackets']),
137                     'total_pkt_bytes': cast_integer(stats['ibytes']),
138                     'pkt_rate': cast_integer(stats['rx_pps']),
139                     'pkt_bit_rate': cast_integer(stats['rx_bps']),
140                     # how many pkts were dropped in RX direction
141                     # need to take the tx counter on the far end port
142                     'dropped_pkts': cast_integer(
143                         far_end_stats['opackets'] - stats['ipackets'])
144                 }
145             }
146             self.__combine_latencies(in_stats, result[ph]['rx'], ph)
147
148         total_tx_pkts = result[0]['tx']['total_pkts'] + result[1]['tx']['total_pkts']
149         result["total_tx_rate"] = cast_integer(total_tx_pkts / self.config.duration_sec)
150         result["flow_stats"] = in_stats["flow_stats"]
151         result["latency"] = in_stats["latency"]
152         return result
153
154     def get_stream_stats(self, trex_stats, if_stats, latencies, chain_idx):
155         """Extract the aggregated stats for a given chain.
156
157         trex_stats: stats as returned by get_stats()
158         if_stats: a list of 2 interface stats to update (port 0 and 1)
159         latencies: a list of 2 Latency instances to update for this chain (port 0 and 1)
160                    latencies[p] is the latency for packets sent on port p
161                    if there are no latency streams, the Latency instances are not modified
162         chain_idx: chain index of the interface stats
163
164         The packet counts include normal and latency streams.
165
166         Trex returns flows stats as follows:
167
168         'flow_stats': {0: {'rx_bps': {0: 0, 1: 0, 'total': 0},
169                    'rx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
170                    'rx_bytes': {0: nan, 1: nan, 'total': nan},
171                    'rx_pkts': {0: 0, 1: 15001, 'total': 15001},
172                    'rx_pps': {0: 0, 1: 0, 'total': 0},
173                    'tx_bps': {0: 0, 1: 0, 'total': 0},
174                    'tx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
175                    'tx_bytes': {0: 1020068, 1: 0, 'total': 1020068},
176                    'tx_pkts': {0: 15001, 1: 0, 'total': 15001},
177                    'tx_pps': {0: 0, 1: 0, 'total': 0}},
178                1: {'rx_bps': {0: 0, 1: 0, 'total': 0},
179                    'rx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
180                    'rx_bytes': {0: nan, 1: nan, 'total': nan},
181                    'rx_pkts': {0: 0, 1: 15001, 'total': 15001},
182                    'rx_pps': {0: 0, 1: 0, 'total': 0},
183                    'tx_bps': {0: 0, 1: 0, 'total': 0},
184                    'tx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
185                    'tx_bytes': {0: 1020068, 1: 0, 'total': 1020068},
186                    'tx_pkts': {0: 15001, 1: 0, 'total': 15001},
187                    'tx_pps': {0: 0, 1: 0, 'total': 0}},
188                 128: {'rx_bps': {0: 0, 1: 0, 'total': 0},
189                 'rx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
190                 'rx_bytes': {0: nan, 1: nan, 'total': nan},
191                 'rx_pkts': {0: 15001, 1: 0, 'total': 15001},
192                 'rx_pps': {0: 0, 1: 0, 'total': 0},
193                 'tx_bps': {0: 0, 1: 0, 'total': 0},
194                 'tx_bps_l1': {0: 0.0, 1: 0.0, 'total': 0.0},
195                 'tx_bytes': {0: 0, 1: 1020068, 'total': 1020068},
196                 'tx_pkts': {0: 0, 1: 15001, 'total': 15001},
197                 'tx_pps': {0: 0, 1: 0, 'total': 0}},etc...
198
199         the pg_id (0, 1, 128,...) is the key of the dict and is obtained using the
200         get_pg_id() method.
201         packet counters for a given stream sent on port p are reported as:
202         - tx_pkts[p] on port p
203         - rx_pkts[1-p] on the far end port
204
205         This is a tricky/critical counter transposition operation because
206         the results are grouped by port (not by stream):
207         tx_pkts_port(p=0) comes from pg_id(port=0, chain_idx)['tx_pkts'][0]
208         rx_pkts_port(p=0) comes from pg_id(port=1, chain_idx)['rx_pkts'][0]
209         tx_pkts_port(p=1) comes from pg_id(port=1, chain_idx)['tx_pkts'][1]
210         rx_pkts_port(p=1) comes from pg_id(port=0, chain_idx)['rx_pkts'][1]
211
212         or using a more generic formula:
213         tx_pkts_port(p) comes from pg_id(port=p, chain_idx)['tx_pkts'][p]
214         rx_pkts_port(p) comes from pg_id(port=1-p, chain_idx)['rx_pkts'][p]
215
216         the second formula is equivalent to
217         rx_pkts_port(1-p) comes from pg_id(port=p, chain_idx)['rx_pkts'][1-p]
218
219         If there are latency streams, those same counters need to be added in the same way
220         """
221         def get_latency(lval):
222             try:
223                 return int(round(lval))
224             except ValueError:
225                 return 0
226
227         for ifs in if_stats:
228             ifs.tx = ifs.rx = 0
229         for port in range(2):
230             pg_id, lat_pg_id = self.get_pg_id(port, chain_idx)
231             for pid in [pg_id, lat_pg_id]:
232                 try:
233                     pg_stats = trex_stats['flow_stats'][pid]
234                     if_stats[port].tx += pg_stats['tx_pkts'][port]
235                     if_stats[1 - port].rx += pg_stats['rx_pkts'][1 - port]
236                 except KeyError:
237                     pass
238             try:
239                 lat = trex_stats['latency'][lat_pg_id]['latency']
240                 # dropped_pkts += lat['err_cntrs']['dropped']
241                 latencies[port].max_usec = get_latency(lat['total_max'])
242                 if math.isnan(lat['total_min']):
243                     latencies[port].min_usec = 0
244                     latencies[port].avg_usec = 0
245                 else:
246                     latencies[port].min_usec = get_latency(lat['total_min'])
247                     latencies[port].avg_usec = get_latency(lat['average'])
248             except KeyError:
249                 pass
250
251     def __combine_latencies(self, in_stats, results, port_handle):
252         """Traverse TRex result dictionary and combines chosen latency stats."""
253         total_max = 0
254         average = 0
255         total_min = float("inf")
256         for chain_id in range(self.chain_count):
257             try:
258                 _, lat_pg_id = self.get_pg_id(port_handle, chain_id)
259                 lat = in_stats['latency'][lat_pg_id]['latency']
260                 # dropped_pkts += lat['err_cntrs']['dropped']
261                 total_max = max(lat['total_max'], total_max)
262                 total_min = min(lat['total_min'], total_min)
263                 average += lat['average']
264             except KeyError:
265                 pass
266         if total_min == float("inf"):
267             total_min = 0
268         results['min_delay_usec'] = total_min
269         results['max_delay_usec'] = total_max
270         results['avg_delay_usec'] = int(average / self.chain_count)
271
272     def _bind_vxlan(self):
273         bind_layers(UDP, VXLAN, dport=4789)
274         bind_layers(VXLAN, Ether)
275
276     def _create_pkt(self, stream_cfg, l2frame_size):
277         """Create a packet of given size.
278
279         l2frame_size: size of the L2 frame in bytes (including the 32-bit FCS)
280         """
281         # Trex will add the FCS field, so we need to remove 4 bytes from the l2 frame size
282         frame_size = int(l2frame_size) - 4
283
284         if stream_cfg['vxlan'] is True:
285             self._bind_vxlan()
286             encap_level = '1'
287             pkt_base = Ether(src=stream_cfg['vtep_src_mac'], dst=stream_cfg['vtep_dst_mac'])
288             if stream_cfg['vtep_vlan'] is not None:
289                 pkt_base /= Dot1Q(vlan=stream_cfg['vtep_vlan'])
290             pkt_base /= IP(src=stream_cfg['vtep_src_ip'], dst=stream_cfg['vtep_dst_ip'])
291             pkt_base /= UDP(sport=random.randint(1337, 32767), dport=4789)
292             pkt_base /= VXLAN(vni=stream_cfg['net_vni'])
293             pkt_base /= Ether(src=stream_cfg['mac_src'], dst=stream_cfg['mac_dst'])
294         else:
295             encap_level = '0'
296             pkt_base = Ether(src=stream_cfg['mac_src'], dst=stream_cfg['mac_dst'])
297
298         if stream_cfg['vlan_tag'] is not None:
299             pkt_base /= Dot1Q(vlan=stream_cfg['vlan_tag'])
300
301         udp_args = {}
302         if stream_cfg['udp_src_port']:
303             udp_args['sport'] = int(stream_cfg['udp_src_port'])
304         if stream_cfg['udp_dst_port']:
305             udp_args['dport'] = int(stream_cfg['udp_dst_port'])
306         pkt_base /= IP() / UDP(**udp_args)
307
308         if stream_cfg['ip_addrs_step'] == 'random':
309             src_fv = STLVmFlowVarRepeatableRandom(
310                 name="ip_src",
311                 min_value=stream_cfg['ip_src_addr'],
312                 max_value=stream_cfg['ip_src_addr_max'],
313                 size=4,
314                 seed=random.randint(0, 32767),
315                 limit=stream_cfg['ip_src_count'])
316             dst_fv = STLVmFlowVarRepeatableRandom(
317                 name="ip_dst",
318                 min_value=stream_cfg['ip_dst_addr'],
319                 max_value=stream_cfg['ip_dst_addr_max'],
320                 size=4,
321                 seed=random.randint(0, 32767),
322                 limit=stream_cfg['ip_dst_count'])
323         else:
324             src_fv = STLVmFlowVar(
325                 name="ip_src",
326                 min_value=stream_cfg['ip_src_addr'],
327                 max_value=stream_cfg['ip_src_addr'],
328                 size=4,
329                 op="inc",
330                 step=stream_cfg['ip_addrs_step'])
331             dst_fv = STLVmFlowVar(
332                 name="ip_dst",
333                 min_value=stream_cfg['ip_dst_addr'],
334                 max_value=stream_cfg['ip_dst_addr_max'],
335                 size=4,
336                 op="inc",
337                 step=stream_cfg['ip_addrs_step'])
338
339         vm_param = [
340             src_fv,
341             STLVmWrFlowVar(fv_name="ip_src", pkt_offset="IP:{}.src".format(encap_level)),
342             dst_fv,
343             STLVmWrFlowVar(fv_name="ip_dst", pkt_offset="IP:{}.dst".format(encap_level)),
344             STLVmFixChecksumHw(l3_offset="IP:{}".format(encap_level),
345                                l4_offset="UDP:{}".format(encap_level),
346                                l4_type=CTRexVmInsFixHwCs.L4_TYPE_UDP)
347         ]
348         pad = max(0, frame_size - len(pkt_base)) * 'x'
349
350         return STLPktBuilder(pkt=pkt_base / pad, vm=STLScVmRaw(vm_param))
351
352     def generate_streams(self, port, chain_id, stream_cfg, l2frame, latency=True,
353                          e2e=False):
354         """Create a list of streams corresponding to a given chain and stream config.
355
356         port: port where the streams originate (0 or 1)
357         chain_id: the chain to which the streams are associated to
358         stream_cfg: stream configuration
359         l2frame: L2 frame size (including 4-byte FCS) or 'IMIX'
360         latency: if True also create a latency stream
361         e2e: True if performing "end to end" connectivity check
362         """
363         streams = []
364         pg_id, lat_pg_id = self.get_pg_id(port, chain_id)
365         if l2frame == 'IMIX':
366             for ratio, l2_frame_size in zip(IMIX_RATIOS, IMIX_L2_SIZES):
367                 pkt = self._create_pkt(stream_cfg, l2_frame_size)
368                 if e2e:
369                     streams.append(STLStream(packet=pkt,
370                                              mode=STLTXCont(pps=ratio)))
371                 else:
372                     if stream_cfg['vxlan'] is True:
373                         streams.append(STLStream(packet=pkt,
374                                                  flow_stats=STLFlowStats(pg_id=pg_id,
375                                                                          vxlan=True),
376                                                  mode=STLTXCont(pps=ratio)))
377                     else:
378                         streams.append(STLStream(packet=pkt,
379                                                  flow_stats=STLFlowStats(pg_id=pg_id),
380                                                  mode=STLTXCont(pps=ratio)))
381
382             if latency:
383                 # for IMIX, the latency packets have the average IMIX packet size
384                 pkt = self._create_pkt(stream_cfg, IMIX_AVG_L2_FRAME_SIZE)
385
386         else:
387             l2frame_size = int(l2frame)
388             pkt = self._create_pkt(stream_cfg, l2frame_size)
389             if e2e:
390                 streams.append(STLStream(packet=pkt,
391                                          mode=STLTXCont()))
392             else:
393                 if stream_cfg['vxlan'] is True:
394                     streams.append(STLStream(packet=pkt,
395                                              flow_stats=STLFlowStats(pg_id=pg_id,
396                                                                      vxlan=True),
397                                              mode=STLTXCont()))
398                 else:
399                     streams.append(STLStream(packet=pkt,
400                                              flow_stats=STLFlowStats(pg_id=pg_id),
401                                              mode=STLTXCont()))
402             # for the latency stream, the minimum payload is 16 bytes even in case of vlan tagging
403             # without vlan, the min l2 frame size is 64
404             # with vlan it is 68
405             # This only applies to the latency stream
406             if latency and stream_cfg['vlan_tag'] and l2frame_size < 68:
407                 pkt = self._create_pkt(stream_cfg, 68)
408
409         if latency:
410             # TRex limitation: VXLAN skip is not supported for latency stream
411             streams.append(STLStream(packet=pkt,
412                                      flow_stats=STLFlowLatencyStats(pg_id=lat_pg_id),
413                                      mode=STLTXCont(pps=self.LATENCY_PPS)))
414         return streams
415
416     @timeout(5)
417     def __connect(self, client):
418         client.connect()
419
420     def __connect_after_start(self):
421         # after start, Trex may take a bit of time to initialize
422         # so we need to retry a few times
423         for it in xrange(self.config.generic_retry_count):
424             try:
425                 time.sleep(1)
426                 self.client.connect()
427                 break
428             except Exception as ex:
429                 if it == (self.config.generic_retry_count - 1):
430                     raise
431                 LOG.info("Retrying connection to TRex (%s)...", ex.message)
432
433     def connect(self):
434         """Connect to the TRex server."""
435         server_ip = self.generator_config.ip
436         LOG.info("Connecting to TRex (%s)...", server_ip)
437
438         # Connect to TRex server
439         self.client = STLClient(server=server_ip, sync_port=self.generator_config.zmq_rpc_port,
440                                 async_port=self.generator_config.zmq_pub_port)
441         try:
442             self.__connect(self.client)
443             if server_ip == '127.0.0.1':
444                 config_updated = self.__check_config()
445                 if config_updated or self.config.restart:
446                     self.__restart()
447         except (TimeoutError, STLError) as e:
448             if server_ip == '127.0.0.1':
449                 self.__start_local_server()
450             else:
451                 raise TrafficGeneratorException(e.message)
452
453         ports = list(self.generator_config.ports)
454         self.port_handle = ports
455         # Prepare the ports
456         self.client.reset(ports)
457         # Read HW information from each port
458         # this returns an array of dict (1 per port)
459         """
460         Example of output for Intel XL710
461         [{'arp': '-', 'src_ipv4': '-', u'supp_speeds': [40000], u'is_link_supported': True,
462           'grat_arp': 'off', 'speed': 40, u'index': 0, 'link_change_supported': 'yes',
463           u'rx': {u'counters': 127, u'caps': [u'flow_stats', u'latency']},
464           u'is_virtual': 'no', 'prom': 'off', 'src_mac': u'3c:fd:fe:a8:24:48', 'status': 'IDLE',
465           u'description': u'Ethernet Controller XL710 for 40GbE QSFP+',
466           'dest': u'fa:16:3e:3c:63:04', u'is_fc_supported': False, 'vlan': '-',
467           u'driver': u'net_i40e', 'led_change_supported': 'yes', 'rx_filter_mode': 'hardware match',
468           'fc': 'none', 'link': 'UP', u'hw_mac': u'3c:fd:fe:a8:24:48', u'pci_addr': u'0000:5e:00.0',
469           'mult': 'off', 'fc_supported': 'no', u'is_led_supported': True, 'rx_queue': 'off',
470           'layer_mode': 'Ethernet', u'numa': 0}, ...]
471         """
472         self.port_info = self.client.get_port_info(ports)
473         LOG.info('Connected to TRex')
474         for id, port in enumerate(self.port_info):
475             LOG.info('   Port %d: %s speed=%dGbps mac=%s pci=%s driver=%s',
476                      id, port['description'], port['speed'], port['src_mac'],
477                      port['pci_addr'], port['driver'])
478         # Make sure the 2 ports have the same speed
479         if self.port_info[0]['speed'] != self.port_info[1]['speed']:
480             raise TrafficGeneratorException('Traffic generator ports speed mismatch: %d/%d Gbps' %
481                                             (self.port_info[0]['speed'],
482                                              self.port_info[1]['speed']))
483
484     def __start_local_server(self):
485         try:
486             LOG.info("Starting TRex ...")
487             self.__start_server()
488             self.__connect_after_start()
489         except (TimeoutError, STLError) as e:
490             LOG.error('Cannot connect to TRex')
491             LOG.error(traceback.format_exc())
492             logpath = '/tmp/trex.log'
493             if os.path.isfile(logpath):
494                 # Wait for TRex to finish writing error message
495                 last_size = 0
496                 for _ in xrange(self.config.generic_retry_count):
497                     size = os.path.getsize(logpath)
498                     if size == last_size:
499                         # probably not writing anymore
500                         break
501                     last_size = size
502                     time.sleep(1)
503                 with open(logpath, 'r') as f:
504                     message = f.read()
505             else:
506                 message = e.message
507             raise TrafficGeneratorException(message)
508
509     def __start_server(self):
510         server = TRexTrafficServer()
511         server.run_server(self.generator_config)
512
513     def __check_config(self):
514         server = TRexTrafficServer()
515         return server.check_config_updated(self.generator_config)
516
517     def __restart(self):
518         LOG.info("Restarting TRex ...")
519         self.__stop_server()
520         # Wait for server stopped
521         for _ in xrange(self.config.generic_retry_count):
522             time.sleep(1)
523             if not self.client.is_connected():
524                 LOG.info("TRex is stopped...")
525                 break
526         self.__start_local_server()
527
528     def __stop_server(self):
529         if self.generator_config.ip == '127.0.0.1':
530             ports = self.client.get_acquired_ports()
531             LOG.info('Release ports %s and stopping TRex...', ports)
532             try:
533                 if ports:
534                     self.client.release(ports=ports)
535                 self.client.server_shutdown()
536             except STLError as e:
537                 LOG.warn('Unable to stop TRex. Error: %s', e)
538         else:
539             LOG.info('Using remote TRex. Unable to stop TRex')
540
541     def resolve_arp(self):
542         """Resolve all configured remote IP addresses.
543
544         return: None if ARP failed to resolve for all IP addresses
545                 else a dict of list of dest macs indexed by port#
546                 the dest macs in the list are indexed by the chain id
547         """
548         self.client.set_service_mode(ports=self.port_handle)
549         LOG.info('Polling ARP until successful...')
550         arp_dest_macs = {}
551         for port, device in zip(self.port_handle, self.generator_config.devices):
552             # there should be 1 stream config per chain
553             stream_configs = device.get_stream_configs()
554             chain_count = len(stream_configs)
555             ctx = self.client.create_service_ctx(port=port)
556             # all dest macs on this port indexed by chain ID
557             dst_macs = [None] * chain_count
558             dst_macs_count = 0
559             # the index in the list is the chain id
560             if self.config.vxlan:
561                 arps = [
562                     ServiceARP(ctx,
563                                src_ip=device.vtep_src_ip,
564                                dst_ip=device.vtep_dst_ip,
565                                vlan=device.vtep_vlan)
566                     for cfg in stream_configs
567                 ]
568             else:
569                 arps = [
570                     ServiceARP(ctx,
571                                src_ip=cfg['ip_src_tg_gw'],
572                                dst_ip=cfg['mac_discovery_gw'],
573                                # will be None if no vlan tagging
574                                vlan=cfg['vlan_tag'])
575                     for cfg in stream_configs
576                 ]
577
578             for attempt in range(self.config.generic_retry_count):
579                 try:
580                     ctx.run(arps)
581                 except STLError:
582                     LOG.error(traceback.format_exc())
583                     continue
584
585                 unresolved = []
586                 for chain_id, mac in enumerate(dst_macs):
587                     if not mac:
588                         arp_record = arps[chain_id].get_record()
589                         if arp_record.dst_mac:
590                             dst_macs[chain_id] = arp_record.dst_mac
591                             dst_macs_count += 1
592                             LOG.info('   ARP: port=%d chain=%d src IP=%s dst IP=%s -> MAC=%s',
593                                      port, chain_id,
594                                      arp_record.src_ip,
595                                      arp_record.dst_ip, arp_record.dst_mac)
596                         else:
597                             unresolved.append(arp_record.dst_ip)
598                 if dst_macs_count == chain_count:
599                     arp_dest_macs[port] = dst_macs
600                     LOG.info('ARP resolved successfully for port %s', port)
601                     break
602                 else:
603                     retry = attempt + 1
604                     LOG.info('Retrying ARP for: %s (retry %d/%d)',
605                              unresolved, retry, self.config.generic_retry_count)
606                     if retry < self.config.generic_retry_count:
607                         time.sleep(self.config.generic_poll_sec)
608             else:
609                 LOG.error('ARP timed out for port %s (resolved %d out of %d)',
610                           port,
611                           dst_macs_count,
612                           chain_count)
613                 break
614
615         self.client.set_service_mode(ports=self.port_handle, enabled=False)
616         if len(arp_dest_macs) == len(self.port_handle):
617             return arp_dest_macs
618         return None
619
620     def __is_rate_enough(self, l2frame_size, rates, bidirectional, latency):
621         """Check if rate provided by user is above requirements. Applies only if latency is True."""
622         intf_speed = self.generator_config.intf_speed
623         if latency:
624             if bidirectional:
625                 mult = 2
626                 total_rate = 0
627                 for rate in rates:
628                     r = utils.convert_rates(l2frame_size, rate, intf_speed)
629                     total_rate += int(r['rate_pps'])
630             else:
631                 mult = 1
632                 total_rate = utils.convert_rates(l2frame_size, rates[0], intf_speed)
633             # rate must be enough for latency stream and at least 1 pps for base stream per chain
634             required_rate = (self.LATENCY_PPS + 1) * self.config.service_chain_count * mult
635             result = utils.convert_rates(l2frame_size,
636                                          {'rate_pps': required_rate},
637                                          intf_speed * mult)
638             result['result'] = total_rate >= required_rate
639             return result
640
641         return {'result': True}
642
643     def create_traffic(self, l2frame_size, rates, bidirectional, latency=True, e2e=False):
644         """Program all the streams in Trex server.
645
646         l2frame_size: L2 frame size or IMIX
647         rates: a list of 2 rates to run each direction
648                each rate is a dict like {'rate_pps': '10kpps'}
649         bidirectional: True if bidirectional
650         latency: True if latency measurement is needed
651         e2e: True if performing "end to end" connectivity check
652         """
653         r = self.__is_rate_enough(l2frame_size, rates, bidirectional, latency)
654         if not r['result']:
655             raise TrafficGeneratorException(
656                 'Required rate in total is at least one of: \n{pps}pps \n{bps}bps \n{load}%.'
657                 .format(pps=r['rate_pps'],
658                         bps=r['rate_bps'],
659                         load=r['rate_percent']))
660         # a dict of list of streams indexed by port#
661         # in case of fixed size, has self.chain_count * 2 * 2 streams
662         # (1 normal + 1 latency stream per direction per chain)
663         # for IMIX, has self.chain_count * 2 * 4 streams
664         # (3 normal + 1 latency stream per direction per chain)
665         streamblock = {}
666         for port in self.port_handle:
667             streamblock[port] = []
668         stream_cfgs = [d.get_stream_configs() for d in self.generator_config.devices]
669         self.rates = [utils.to_rate_str(rate) for rate in rates]
670         for chain_id, (fwd_stream_cfg, rev_stream_cfg) in enumerate(zip(*stream_cfgs)):
671             streamblock[0].extend(self.generate_streams(self.port_handle[0],
672                                                         chain_id,
673                                                         fwd_stream_cfg,
674                                                         l2frame_size,
675                                                         latency=latency,
676                                                         e2e=e2e))
677             if len(self.rates) > 1:
678                 streamblock[1].extend(self.generate_streams(self.port_handle[1],
679                                                             chain_id,
680                                                             rev_stream_cfg,
681                                                             l2frame_size,
682                                                             latency=bidirectional and latency,
683                                                             e2e=e2e))
684
685         for port in self.port_handle:
686             if self.config.vxlan:
687                 self.client.set_port_attr(ports=port, vxlan_fs=[4789])
688             else:
689                 self.client.set_port_attr(ports=port, vxlan_fs=None)
690             self.client.add_streams(streamblock[port], ports=port)
691             LOG.info('Created %d traffic streams for port %s.', len(streamblock[port]), port)
692
693     def clear_streamblock(self):
694         """Clear all streams from TRex."""
695         self.rates = []
696         self.client.reset(self.port_handle)
697         LOG.info('Cleared all existing streams')
698
699     def get_stats(self):
700         """Get stats from Trex."""
701         stats = self.client.get_stats()
702         return self.extract_stats(stats)
703
704     def get_macs(self):
705         """Return the Trex local port MAC addresses.
706
707         return: a list of MAC addresses indexed by the port#
708         """
709         return [port['src_mac'] for port in self.port_info]
710
711     def get_port_speed_gbps(self):
712         """Return the Trex local port MAC addresses.
713
714         return: a list of speed in Gbps indexed by the port#
715         """
716         return [port['speed'] for port in self.port_info]
717
718     def clear_stats(self):
719         """Clear all stats in the traffic gneerator."""
720         if self.port_handle:
721             self.client.clear_stats()
722
723     def start_traffic(self):
724         """Start generating traffic in all ports."""
725         for port, rate in zip(self.port_handle, self.rates):
726             self.client.start(ports=port, mult=rate, duration=self.config.duration_sec, force=True)
727
728     def stop_traffic(self):
729         """Stop generating traffic."""
730         self.client.stop(ports=self.port_handle)
731
732     def start_capture(self):
733         """Capture all packets on both ports that are unicast to us."""
734         if self.capture_id:
735             self.stop_capture()
736         # Need to filter out unwanted packets so we do not end up counting
737         # src MACs of frames that are not unicast to us
738         src_mac_list = self.get_macs()
739         bpf_filter = "ether dst %s or ether dst %s" % (src_mac_list[0], src_mac_list[1])
740         # ports must be set in service in order to enable capture
741         self.client.set_service_mode(ports=self.port_handle)
742         self.capture_id = self.client.start_capture(rx_ports=self.port_handle,
743                                                     bpf_filter=bpf_filter)
744
745     def fetch_capture_packets(self):
746         """Fetch capture packets in capture mode."""
747         if self.capture_id:
748             self.packet_list = []
749             self.client.fetch_capture_packets(capture_id=self.capture_id['id'],
750                                               output=self.packet_list)
751
752     def stop_capture(self):
753         """Stop capturing packets."""
754         if self.capture_id:
755             self.client.stop_capture(capture_id=self.capture_id['id'])
756             self.capture_id = None
757             self.client.set_service_mode(ports=self.port_handle, enabled=False)
758
759     def cleanup(self):
760         """Cleanup Trex driver."""
761         if self.client:
762             try:
763                 self.client.reset(self.port_handle)
764                 self.client.disconnect()
765             except STLError:
766                 # TRex does not like a reset while in disconnected state
767                 pass