Merge "load_gen: Supporting loading of load_gen via loader."
[vswitchperf.git] / tools / pkt_gen / trex / trex.py
1 # Copyright 2017 Martin Goldammer, OPNFV, Red Hat Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain 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,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 """
16 Trex Traffic Generator Model
17 """
18 # pylint: disable=undefined-variable
19 import logging
20 import subprocess
21 import sys
22 from collections import OrderedDict
23 # pylint: disable=unused-import
24 import netaddr
25 import zmq
26 from conf import settings
27 from conf import merge_spec
28 from core.results.results_constants import ResultsConstants
29 from tools.pkt_gen.trafficgen.trafficgen import ITrafficGenerator
30 # pylint: disable=wrong-import-position, import-error
31 sys.path.append(settings.getValue('PATHS')['trafficgen']['trex']['src']['path'])
32 from trex_stl_lib.api import *
33
34 _EMPTY_STATS = {
35     'global': {'bw_per_core': 0.0,
36                'cpu_util': 0.0,
37                'queue_full': 0.0,
38                'rx_bps': 0.0,
39                'rx_cpu_util': 0.0,
40                'rx_drop_bps': 0.0,
41                'rx_pps': 0.0,
42                'tx_bps': 0.0,
43                'tx_pps': 0.0,},
44     'latency': {},
45     'total': {'ibytes': 0.0,
46               'ierrors': 0.0,
47               'ipackets': 0.0,
48               'obytes': 0.0,
49               'oerrors': 0.0,
50               'opackets': 0.0,
51               'rx_bps': 0.0,
52               'rx_bps_L1': 0.0,
53               'rx_pps': 0.0,
54               'rx_util': 0.0,
55               'tx_bps': 0.0,
56               'tx_bps_L1': 0.0,
57               'tx_pps': 0.0,
58               'tx_util': 0.0,}}
59
60 class Trex(ITrafficGenerator):
61     """Trex Traffic generator wrapper."""
62     _logger = logging.getLogger(__name__)
63
64     def __init__(self):
65         """Trex class constructor."""
66         super().__init__()
67         self._logger.info("In trex __init__ method")
68         self._params = {}
69         self._trex_host_ip_addr = (
70             settings.getValue('TRAFFICGEN_TREX_HOST_IP_ADDR'))
71         self._trex_base_dir = (
72             settings.getValue('TRAFFICGEN_TREX_BASE_DIR'))
73         self._trex_user = settings.getValue('TRAFFICGEN_TREX_USER')
74         self._stlclient = None
75
76     def connect(self):
77         '''Connect to Trex traffic generator
78
79         Verify that Trex is on the system indicated by
80         the configuration file
81         '''
82         self._stlclient = STLClient()
83         self._logger.info("TREX:  In Trex connect method...")
84         if self._trex_host_ip_addr:
85             cmd_ping = "ping -c1 " + self._trex_host_ip_addr
86         else:
87             raise RuntimeError('TREX: Trex host not defined')
88
89         ping = subprocess.Popen(cmd_ping, shell=True, stderr=subprocess.PIPE)
90         output, error = ping.communicate()
91
92         if ping.returncode:
93             self._logger.error(error)
94             self._logger.error(output)
95             raise RuntimeError('TREX: Cannot ping Trex host at ' + \
96                                self._trex_host_ip_addr)
97
98         connect_trex = "ssh " + self._trex_user + \
99                           "@" + self._trex_host_ip_addr
100
101         cmd_find_trex = connect_trex + " ls " + \
102                           self._trex_base_dir + "t-rex-64"
103
104
105         find_trex = subprocess.Popen(cmd_find_trex,
106                                      shell=True,
107                                      stderr=subprocess.PIPE)
108         output, error = find_trex.communicate()
109
110         if find_trex.returncode:
111             self._logger.error(error)
112             self._logger.error(output)
113             raise RuntimeError(
114                 'TREX: Cannot locate Trex program at %s within %s' \
115                 % (self._trex_host_ip_addr, self._trex_base_dir))
116
117         self._stlclient = STLClient(username=self._trex_user, server=self._trex_host_ip_addr,
118                                     verbose_level=0)
119         self._stlclient.connect()
120         self._logger.info("TREX: Trex host successfully found...")
121
122     def disconnect(self):
123         """Disconnect from the traffic generator.
124
125         As with :func:`connect`, this function is optional.
126
127         Where implemented, this function should raise an exception on
128         failure.
129
130         :returns: None
131         """
132         self._logger.info("TREX: In trex disconnect method")
133         self._stlclient.disconnect(stop_traffic=True, release_ports=True)
134
135     @staticmethod
136     def create_packets(traffic, ports_info):
137         """Create base packet according to traffic specification.
138            If traffic haven't specified srcmac and dstmac fields
139            packet will be create with mac address of trex server.
140         """
141         mac_add = [li['hw_mac'] for li in ports_info]
142
143         if traffic and traffic['l2']['framesize'] > 0:
144             if traffic['l2']['dstmac'] == '00:00:00:00:00:00' and \
145                traffic['l2']['srcmac'] == '00:00:00:00:00:00':
146                 base_pkt_a = Ether(src=mac_add[0], dst=mac_add[1])/ \
147                              IP(proto=traffic['l3']['proto'], src=traffic['l3']['srcip'],
148                                 dst=traffic['l3']['dstip'])/ \
149                              UDP(dport=traffic['l4']['dstport'], sport=traffic['l4']['srcport'])
150                 base_pkt_b = Ether(src=mac_add[1], dst=mac_add[0])/ \
151                              IP(proto=traffic['l3']['proto'], src=traffic['l3']['dstip'],
152                                 dst=traffic['l3']['srcip'])/ \
153                              UDP(dport=traffic['l4']['srcport'], sport=traffic['l4']['dstport'])
154             else:
155                 base_pkt_a = Ether(src=traffic['l2']['srcmac'], dst=traffic['l2']['dstmac'])/ \
156                              IP(proto=traffic['l3']['proto'], src=traffic['l3']['dstip'],
157                                 dst=traffic['l3']['srcip'])/ \
158                              UDP(dport=traffic['l4']['dstport'], sport=traffic['l4']['srcport'])
159
160                 base_pkt_b = Ether(src=traffic['l2']['dstmac'], dst=traffic['l2']['srcmac'])/ \
161                              IP(proto=traffic['l3']['proto'], src=traffic['l3']['dstip'],
162                                 dst=traffic['l3']['srcip'])/ \
163                              UDP(dport=traffic['l4']['srcport'], sport=traffic['l4']['dstport'])
164
165         return (base_pkt_a, base_pkt_b)
166
167     @staticmethod
168     def create_streams(base_pkt_a, base_pkt_b, traffic):
169         """Add the base packet to the streams. Erase FCS and add payload
170            according to traffic specification
171         """
172         stream_1_lat = None
173         stream_2_lat = None
174         frame_size = int(traffic['l2']['framesize'])
175         fsize_no_fcs = frame_size - 4
176         payload_a = max(0, fsize_no_fcs - len(base_pkt_a)) * 'x'
177         payload_b = max(0, fsize_no_fcs - len(base_pkt_b)) * 'x'
178
179         # Multistream configuration, increments source values only
180         ms_mod = list() # mod list for incrementing values to be populated based on layer
181         if traffic['multistream'] > 1:
182             if traffic['stream_type'].upper() == 'L2':
183                 for _ in [base_pkt_a, base_pkt_b]:
184                     ms_mod += [STLVmFlowVar(name="mac_start", min_value=0,
185                                             max_value=traffic['multistream'] - 1, size=4, op="inc"),
186                                STLVmWrFlowVar(fv_name="mac_start", pkt_offset=7)]
187             elif traffic['stream_type'].upper() == 'L3':
188                 ip_src = {"start": int(netaddr.IPAddress(traffic['l3']['srcip'])),
189                           "end": int(netaddr.IPAddress(traffic['l3']['srcip'])) + traffic['multistream'] - 1}
190                 ip_dst = {"start": int(netaddr.IPAddress(traffic['l3']['dstip'])),
191                           "end": int(netaddr.IPAddress(traffic['l3']['dstip'])) + traffic['multistream'] - 1}
192                 for ip_address in [ip_src, ip_dst]:
193                     ms_mod += [STLVmFlowVar(name="ip_src", min_value=ip_address['start'],
194                                             max_value=ip_address['end'], size=4, op="inc"),
195                                STLVmWrFlowVar(fv_name="ip_src", pkt_offset="IP.src")]
196             elif traffic['stream_type'].upper() == 'L4':
197                 for udpport in [traffic['l4']['srcport'], traffic['l4']['dstport']]:
198                     if udpport + (traffic['multistream'] - 1) > 65535:
199                         start_port = udpport
200                         # find the max/min port number based on the loop around of 65535 to 0 if needed
201                         minimum_value = 65535 - (traffic['multistream'] -1)
202                         maximum_value = 65535
203                     else:
204                         start_port, minimum_value = udpport, udpport
205                         maximum_value = start_port + (traffic['multistream'] - 1)
206                     ms_mod += [STLVmFlowVar(name="port_src", init_value=start_port,
207                                             min_value=minimum_value, max_value=maximum_value,
208                                             size=2, op="inc"),
209                                STLVmWrFlowVar(fv_name="port_src", pkt_offset="UDP.sport"),]
210
211         if ms_mod: # multistream detected
212             pkt_a = STLPktBuilder(pkt=base_pkt_a/payload_a, vm=[ms_mod[0], ms_mod[1]])
213             pkt_b = STLPktBuilder(pkt=base_pkt_b/payload_b, vm=[ms_mod[2], ms_mod[3]])
214         else:
215             pkt_a = STLPktBuilder(pkt=base_pkt_a / payload_a)
216             pkt_b = STLPktBuilder(pkt=base_pkt_b / payload_b)
217
218         stream_1 = STLStream(packet=pkt_a,
219                              name='stream_1',
220                              mode=STLTXCont(percentage=traffic['frame_rate']))
221         stream_2 = STLStream(packet=pkt_b,
222                              name='stream_2',
223                              mode=STLTXCont(percentage=traffic['frame_rate']))
224         lat_pps = settings.getValue('TRAFFICGEN_TREX_LATENCY_PPS')
225         if lat_pps > 0:
226             stream_1_lat = STLStream(packet=pkt_a,
227                                      flow_stats=STLFlowLatencyStats(pg_id=0),
228                                      name='stream_1_lat',
229                                      mode=STLTXCont(pps=lat_pps))
230             stream_2_lat = STLStream(packet=pkt_b,
231                                      flow_stats=STLFlowLatencyStats(pg_id=1),
232                                      name='stream_2_lat',
233                                      mode=STLTXCont(pps=lat_pps))
234
235         return (stream_1, stream_2, stream_1_lat, stream_2_lat)
236
237     def generate_traffic(self, traffic, duration):
238         """The method that generate a stream
239         """
240         my_ports = [0, 1]
241         self._stlclient.reset(my_ports)
242         ports_info = self._stlclient.get_port_info(my_ports)
243         # for SR-IOV
244         if settings.getValue('TRAFFICGEN_TREX_PROMISCUOUS'):
245             self._stlclient.set_port_attr(my_ports, promiscuous=True)
246
247         packet_1, packet_2 = Trex.create_packets(traffic, ports_info)
248         stream_1, stream_2, stream_1_lat, stream_2_lat = Trex.create_streams(packet_1, packet_2, traffic)
249         self._stlclient.add_streams(stream_1, ports=[0])
250         self._stlclient.add_streams(stream_2, ports=[1])
251
252         if stream_1_lat is not None:
253             self._stlclient.add_streams(stream_1_lat, ports=[0])
254             self._stlclient.add_streams(stream_2_lat, ports=[1])
255
256         self._stlclient.clear_stats()
257         self._stlclient.start(ports=[0, 1], force=True, duration=duration)
258         self._stlclient.wait_on_traffic(ports=[0, 1])
259         stats = self._stlclient.get_stats(sync_now=True)
260         return stats
261
262     @staticmethod
263     def calculate_results(stats):
264         """Calculate results from Trex statistic
265         """
266         result = OrderedDict()
267         result[ResultsConstants.TX_RATE_FPS] = (
268             '{:.3f}'.format(
269                 float(stats["total"]["tx_pps"])))
270
271         result[ResultsConstants.THROUGHPUT_RX_FPS] = (
272             '{:.3f}'.format(
273                 float(stats["total"]["rx_pps"])))
274
275         result[ResultsConstants.TX_RATE_MBPS] = (
276             '{:.3f}'.format(
277                 float(stats["total"]["tx_bps"] / 1000000)))
278         result[ResultsConstants.THROUGHPUT_RX_MBPS] = (
279             '{:.3f}'.format(
280                 float(stats["total"]["rx_bps"] / 1000000)))
281
282         result[ResultsConstants.TX_RATE_PERCENT] = 'Unknown'
283
284         result[ResultsConstants.THROUGHPUT_RX_PERCENT] = 'Unknown'
285         if stats["total"]["opackets"]:
286             result[ResultsConstants.FRAME_LOSS_PERCENT] = (
287                 '{:.3f}'.format(
288                     float((stats["total"]["opackets"] - stats["total"]["ipackets"]) * 100 /
289                           stats["total"]["opackets"])))
290         else:
291             result[ResultsConstants.FRAME_LOSS_PERCENT] = 100
292
293         if settings.getValue('TRAFFICGEN_TREX_LATENCY_PPS') > 0 and stats['latency']:
294             result[ResultsConstants.MIN_LATENCY_NS] = (
295                 '{:.3f}'.format(
296                     (float(min(stats["latency"][0]["latency"]["total_min"],
297                                stats["latency"][1]["latency"]["total_min"])))))
298
299             result[ResultsConstants.MAX_LATENCY_NS] = (
300                 '{:.3f}'.format(
301                     (float(max(stats["latency"][0]["latency"]["total_max"],
302                                stats["latency"][1]["latency"]["total_max"])))))
303
304             result[ResultsConstants.AVG_LATENCY_NS] = (
305                 '{:.3f}'.format(
306                     float((stats["latency"][0]["latency"]["average"]+
307                            stats["latency"][1]["latency"]["average"])/2)))
308
309         else:
310             result[ResultsConstants.MIN_LATENCY_NS] = 'Unknown'
311             result[ResultsConstants.MAX_LATENCY_NS] = 'Unknown'
312             result[ResultsConstants.AVG_LATENCY_NS] = 'Unknown'
313         return result
314
315     def send_cont_traffic(self, traffic=None, duration=30):
316         """See ITrafficGenerator for description
317         """
318         self._logger.info("In Trex send_cont_traffic method")
319         self._params.clear()
320
321         self._params['traffic'] = self.traffic_defaults.copy()
322         if traffic:
323             self._params['traffic'] = merge_spec(
324                 self._params['traffic'], traffic)
325
326         stats = self.generate_traffic(traffic, duration)
327
328         return self.calculate_results(stats)
329
330     def start_cont_traffic(self, traffic=None, duration=30):
331         raise NotImplementedError(
332             'Trex start cont traffic not implemented')
333
334     def stop_cont_traffic(self):
335         """See ITrafficGenerator for description
336         """
337         raise NotImplementedError(
338             'Trex stop_cont_traffic method not implemented')
339
340     def send_rfc2544_throughput(self, traffic=None, duration=60,
341                                 lossrate=0.0, tests=10):
342         """See ITrafficGenerator for description
343         """
344         self._logger.info("In Trex send_rfc2544_throughput method")
345         self._params.clear()
346         threshold = settings.getValue('TRAFFICGEN_TREX_RFC2544_TPUT_THRESHOLD')
347         test_lossrate = 0
348         left = 0
349         iteration = 1
350         stats_ok = _EMPTY_STATS
351         self._params['traffic'] = self.traffic_defaults.copy()
352         if traffic:
353             self._params['traffic'] = merge_spec(
354                 self._params['traffic'], traffic)
355         new_params = copy.deepcopy(traffic)
356         stats = self.generate_traffic(traffic, duration)
357         right = traffic['frame_rate']
358         center = traffic['frame_rate']
359
360         # Loops until the preconfigured difference between frame rate
361         # of successful and unsuccessful iterations is reached
362         while (right - left) > threshold:
363             test_lossrate = ((stats["total"]["opackets"] - stats["total"]
364                               ["ipackets"]) * 100) / stats["total"]["opackets"]
365             self._logger.debug("Iteration: %s, frame rate: %s, throughput_rx_fps: %s, frame_loss_percent: %s",
366                                iteration, "{:.3f}".format(new_params['frame_rate']), stats['total']['rx_pps'],
367                                "{:.3f}".format(test_lossrate))
368             if test_lossrate == 0.0 and new_params['frame_rate'] == traffic['frame_rate']:
369                 stats_ok = copy.deepcopy(stats)
370                 break
371             elif test_lossrate > lossrate:
372                 right = center
373                 center = (left+right) / 2
374                 new_params = copy.deepcopy(traffic)
375                 new_params['frame_rate'] = center
376                 stats = self.generate_traffic(new_params, duration)
377             else:
378                 stats_ok = copy.deepcopy(stats)
379                 left = center
380                 center = (left+right) / 2
381                 new_params = copy.deepcopy(traffic)
382                 new_params['frame_rate'] = center
383                 stats = self.generate_traffic(new_params, duration)
384             iteration += 1
385         return self.calculate_results(stats_ok)
386
387     def start_rfc2544_throughput(self, traffic=None, tests=1, duration=60,
388                                  lossrate=0.0):
389         raise NotImplementedError(
390             'Trex start rfc2544 throughput not implemented')
391
392     def wait_rfc2544_throughput(self):
393         raise NotImplementedError(
394             'Trex wait rfc2544 throughput not implemented')
395
396     def send_burst_traffic(self, traffic=None, numpkts=100, duration=5):
397         raise NotImplementedError(
398             'Trex send burst traffic not implemented')
399
400     def send_rfc2544_back2back(self, traffic=None, tests=1, duration=30,
401                                lossrate=0.0):
402         raise NotImplementedError(
403             'Trex send rfc2544 back2back not implemented')
404
405     def start_rfc2544_back2back(self, traffic=None, tests=1, duration=30,
406                                 lossrate=0.0):
407         raise NotImplementedError(
408             'Trex start rfc2544 back2back not implemented')
409
410     def wait_rfc2544_back2back(self):
411         raise NotImplementedError(
412             'Trex wait rfc2544 back2back not implemented')
413
414 if __name__ == "__main__":
415     pass