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