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