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