fix(stl_traffic): track both ports on unidir
[csit.git] / GPL / tools / trex / trex_stl_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2022 Cisco and/or its affiliates.
4 #
5 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
6 #
7 # Licensed under the Apache License 2.0 or
8 # GNU General Public License v2.0 or later;  you may not use this file
9 # except in compliance with one of these Licenses. You
10 # may obtain a copy of the Licenses at:
11 #
12 #     http://www.apache.org/licenses/LICENSE-2.0
13 #     https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
14 #
15 # Note: If this file is linked with Scapy, which is GPLv2+, your use of it
16 # must be under GPLv2+.  If at any point in the future it is no longer linked
17 # with Scapy (or other GPLv2+ licensed software), you are free to choose
18 # Apache 2.
19 #
20 # Unless required by applicable law or agreed to in writing, software
21 # distributed under the License is distributed on an "AS IS" BASIS,
22 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 # See the License for the specific language governing permissions and
24 # limitations under the License.
25
26 """This module gets a traffic profile together with other parameters, reads
27 the profile and sends the traffic. At the end, it measures the packet loss and
28 latency.
29 """
30
31 import argparse
32 import json
33 import sys
34 import time
35
36 sys.path.insert(
37     0, u"/opt/trex-core-2.97/scripts/automation/trex_control_plane/interactive/"
38 )
39 from trex.stl.api import STLClient, STLProfile, STLError
40
41
42 def fmt_latency(lat_min, lat_avg, lat_max, hdrh):
43     """Return formatted, rounded latency.
44
45     :param lat_min: Min latency
46     :param lat_avg: Average latency
47     :param lat_max: Max latency
48     :param hdrh: Base64 encoded compressed HDRHistogram object.
49     :type lat_min: str
50     :type lat_avg: str
51     :type lat_max: str
52     :type hdrh: str
53     :return: Formatted and rounded output (hdrh unchanged) "min/avg/max/hdrh".
54     :rtype: str
55     """
56     try:
57         t_min = int(round(float(lat_min)))
58     except ValueError:
59         t_min = int(-1)
60     try:
61         t_avg = int(round(float(lat_avg)))
62     except ValueError:
63         t_avg = int(-1)
64     try:
65         t_max = int(round(float(lat_max)))
66     except ValueError:
67         t_max = int(-1)
68
69     return u"/".join(str(tmp) for tmp in (t_min, t_avg, t_max, hdrh))
70
71
72 def simple_burst(
73         profile_file,
74         duration,
75         framesize,
76         rate,
77         port_0,
78         port_1,
79         latency,
80         async_start=False,
81         traffic_directions=2,
82         force=False,
83         delay=0.0,
84     ):
85     """Send traffic and measure packet loss and latency.
86
87     Procedure:
88      - reads the given traffic profile with streams,
89      - connects to the T-rex client,
90      - resets the ports,
91      - removes all existing streams,
92      - adds streams from the traffic profile to the ports,
93      - if the warm-up time is more than 0, sends the warm-up traffic, reads the
94        statistics,
95      - clears the statistics from the client,
96      - starts the traffic,
97      - waits for the defined time (or runs forever if async mode is defined),
98      - stops the traffic,
99      - reads and displays the statistics and
100      - disconnects from the client.
101
102     :param profile_file: A python module with T-rex traffic profile.
103     :param framesize: Frame size.
104     :param duration: Duration of traffic run in seconds (-1=infinite).
105     :param rate: Traffic rate [percentage, pps, bps].
106     :param port_0: Port 0 on the traffic generator.
107     :param port_1: Port 1 on the traffic generator.
108     :param latency: With latency stats.
109     :param async_start: Start the traffic and exit.
110     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
111     :param force: Force start regardless of ports state.
112     :param delay: Sleep overhead [s].
113     :type profile_file: str
114     :type framesize: int or str
115     :type duration: float
116     :type rate: str
117     :type port_0: int
118     :type port_1: int
119     :type latency: bool
120     :type async_start: bool
121     :type traffic_directions: int
122     :type force: bool
123     :type delay: float
124     """
125     client = None
126     total_rcvd = 0
127     total_sent = 0
128     approximated_duration = 0.0
129     lost_a = 0
130     lost_b = 0
131     lat_a = u"-1/-1/-1/"
132     lat_b = u"-1/-1/-1/"
133
134     # Read the profile:
135     try:
136         print(f"### Profile file:\n{profile_file}")
137         profile = STLProfile.load(
138             profile_file, direction=0, port_id=0, framesize=framesize,
139             rate=rate
140         )
141         streams = profile.get_streams()
142     except STLError:
143         print(f"Error while loading profile '{profile_file}'!")
144         raise
145
146     try:
147         # Create the client:
148         client = STLClient()
149         # Connect to server:
150         client.connect()
151         # Prepare our ports (the machine has 0 <--> 1 with static route):
152         client.reset(ports=[port_0, port_1])
153         client.remove_all_streams(ports=[port_0, port_1])
154
155         if u"macsrc" in profile_file:
156             client.set_port_attr(ports=[port_0, port_1], promiscuous=True)
157         if isinstance(framesize, int):
158             last_stream_a = int((len(streams) - 2) / 2)
159             last_stream_b = (last_stream_a * 2)
160             client.add_streams(streams[0:last_stream_a], ports=[port_0])
161             if traffic_directions > 1:
162                 client.add_streams(
163                     streams[last_stream_a:last_stream_b], ports=[port_1])
164         elif isinstance(framesize, str):
165             client.add_streams(streams[0:3], ports=[port_0])
166             if traffic_directions > 1:
167                 client.add_streams(streams[3:6], ports=[port_1])
168         if latency:
169             try:
170                 if isinstance(framesize, int):
171                     client.add_streams(streams[last_stream_b], ports=[port_0])
172                     if traffic_directions > 1:
173                         client.add_streams(
174                             streams[last_stream_b + 1], ports=[port_1])
175                 elif isinstance(framesize, str):
176                     latency = False
177             except STLError:
178                 # Disable latency if NIC does not support requested stream type
179                 print(u"##### FAILED to add latency streams #####")
180                 latency = False
181         # Even for unidir, both ports are needed to see both rx and tx.
182         ports = [port_0, port_1]
183
184         # Clear the stats before injecting:
185         client.clear_stats()
186         lost_a = 0
187         lost_b = 0
188
189         # Choose rate and start traffic:
190         client.start(
191             ports=ports[:traffic_directions],
192             mult=rate,
193             duration=duration,
194             force=force,
195             core_mask=STLClient.CORE_MASK_PIN,
196         )
197
198         if async_start:
199             # For async stop, we need to export the current snapshot.
200             xsnap0 = client.ports[0].get_xstats().reference_stats
201             print(f"Xstats snapshot 0: {xsnap0!r}")
202             xsnap1 = client.ports[1].get_xstats().reference_stats
203             print(f"Xstats snapshot 1: {xsnap1!r}")
204         else:
205             time_start = time.monotonic()
206             # wait_on_traffic fails if duration stretches by 30 seconds or more.
207             # TRex has some overhead, wait some more.
208             time.sleep(duration + delay)
209             client.stop()
210             time_stop = time.monotonic()
211             approximated_duration = time_stop - time_start - delay
212             # Read the stats after the traffic stopped (or time up).
213             stats = client.get_stats()
214             if client.get_warnings():
215                 for warning in client.get_warnings():
216                     print(warning)
217             # Now finish the complete reset.
218             client.reset()
219
220             print(u"##### Statistics #####")
221             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
222
223             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
224             lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
225
226             # Stats index is not a port number, but "pgid".
227             if latency:
228                 lat_obj = stats[u"latency"][0][u"latency"]
229                 lat_a = fmt_latency(
230                     str(lat_obj[u"total_min"]), str(lat_obj[u"average"]),
231                     str(lat_obj[u"total_max"]), str(lat_obj[u"hdrh"]))
232                 # Do not bother with the other dir latency if unidir.
233                 if traffic_directions > 1:
234                     lat_obj = stats[u"latency"][1][u"latency"]
235                     lat_b = fmt_latency(
236                         str(lat_obj[u"total_min"]), str(lat_obj[u"average"]),
237                         str(lat_obj[u"total_max"]), str(lat_obj[u"hdrh"]))
238
239             total_sent = stats[0][u"opackets"] + stats[1][u"opackets"]
240             total_rcvd = stats[0][u"ipackets"] + stats[1][u"ipackets"]
241
242             print(f"\npackets lost from {port_0} --> {port_1}: {lost_a} pkts")
243             print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
244
245     except STLError:
246         print(u"T-Rex STL runtime error!", file=sys.stderr)
247         raise
248
249     finally:
250         if async_start:
251             if client:
252                 client.disconnect(stop_traffic=False, release_ports=True)
253         else:
254             if client:
255                 client.disconnect()
256             print(
257                 f"rate={rate!r}; "
258                 f"total_received={total_rcvd}; "
259                 f"total_sent={total_sent}; "
260                 f"frame_loss={lost_a + lost_b}; "
261                 f"target_duration={duration!r}; "
262                 f"approximated_duration={approximated_duration!r}; "
263                 f"latency_stream_0(usec)={lat_a}; "
264                 f"latency_stream_1(usec)={lat_b}; "
265             )
266
267
268 def main():
269     """Main function for the traffic generator using T-rex.
270
271     It verifies the given command line arguments and runs "simple_burst"
272     function.
273     """
274     parser = argparse.ArgumentParser()
275     parser.add_argument(
276         u"-p", u"--profile", required=True, type=str,
277         help=u"Python traffic profile."
278     )
279     parser.add_argument(
280         u"-d", u"--duration", required=True, type=float,
281         help=u"Duration of traffic run."
282     )
283     parser.add_argument(
284         u"-s", u"--frame_size", required=True,
285         help=u"Size of a Frame without padding and IPG."
286     )
287     parser.add_argument(
288         u"-r", u"--rate", required=True,
289         help=u"Traffic rate with included units (pps)."
290     )
291     parser.add_argument(
292         u"--port_0", required=True, type=int,
293         help=u"Port 0 on the traffic generator."
294     )
295     parser.add_argument(
296         u"--port_1", required=True, type=int,
297         help=u"Port 1 on the traffic generator."
298     )
299     parser.add_argument(
300         u"--async_start", action=u"store_true", default=False,
301         help=u"Non-blocking call of the script."
302     )
303     parser.add_argument(
304         u"--latency", action=u"store_true", default=False,
305         help=u"Add latency stream."
306     )
307     parser.add_argument(
308         u"--traffic_directions", type=int, default=2,
309         help=u"Send bi- (2) or uni- (1) directional traffic."
310     )
311     parser.add_argument(
312         u"--force", action=u"store_true", default=False,
313         help=u"Force start regardless of ports state."
314     )
315     parser.add_argument(
316         u"--delay", required=True, type=float, default=0.0,
317         help=u"Delay assumed for traffic, sleep time is increased by this [s]."
318     )
319
320     args = parser.parse_args()
321
322     try:
323         framesize = int(args.frame_size)
324     except ValueError:
325         framesize = args.frame_size
326
327     simple_burst(
328         profile_file=args.profile,
329         duration=args.duration,
330         framesize=framesize,
331         rate=args.rate,
332         port_0=args.port_0,
333         port_1=args.port_1,
334         latency=args.latency,
335         async_start=args.async_start,
336         traffic_directions=args.traffic_directions,
337         force=args.force,
338         delay=args.delay,
339     )
340
341
342 if __name__ == u"__main__":
343     main()