T-Rex: Add advanced stateful mode
[csit.git] / GPL / tools / trex / trex_astf_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2020 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 """This module gets T-Rex advanced stateful (astf) traffic profile together
17 with other parameters, reads the profile and sends the traffic. At the end, it
18 measures the packet loss and latency.
19 """
20
21 import argparse
22 import json
23 import sys
24 import time
25
26 sys.path.insert(
27     0, u"/opt/trex-core-2.73/scripts/automation/trex_control_plane/interactive/"
28 )
29 from trex.astf.api import *
30
31
32 def fmt_latency(lat_min, lat_avg, lat_max, hdrh):
33     """Return formatted, rounded latency.
34
35     :param lat_min: Min latency
36     :param lat_avg: Average latency
37     :param lat_max: Max latency
38     :param hdrh: Base64 encoded compressed HDRHistogram object.
39     :type lat_min: str
40     :type lat_avg: str
41     :type lat_max: str
42     :type hdrh: str
43     :return: Formatted and rounded output (hdrh unchanged) "min/avg/max/hdrh".
44     :rtype: str
45     """
46     try:
47         t_min = int(round(float(lat_min)))
48     except ValueError:
49         t_min = int(-1)
50     try:
51         t_avg = int(round(float(lat_avg)))
52     except ValueError:
53         t_avg = int(-1)
54     try:
55         t_max = int(round(float(lat_max)))
56     except ValueError:
57         t_max = int(-1)
58
59     return u"/".join(str(tmp) for tmp in (t_min, t_avg, t_max, hdrh))
60
61
62 def simple_burst(
63         profile_file, duration, framesize, mult, warmup_time, port_0, port_1,
64         latency, async_start=False, traffic_directions=2):
65     """Send traffic and measure packet loss and latency.
66
67     Procedure:
68      - reads the given traffic profile with streams,
69      - connects to the T-rex astf client,
70      - resets the ports,
71      - removes all existing streams,
72      - adds streams from the traffic profile to the ports,
73      - if the warm-up time is more than 0, sends the warm-up traffic, reads the
74        statistics,
75      - clears the statistics from the client,
76      - starts the traffic,
77      - waits for the defined time (or runs forever if async mode is defined),
78      - stops the traffic,
79      - reads and displays the statistics and
80      - disconnects from the client.
81
82     :param profile_file: A python module with T-rex traffic profile.
83     :param duration: Duration of traffic run in seconds (-1=infinite).
84     :param framesize: Frame size.
85     :param mult: Multiplier of profile CPS.
86     :param warmup_time: Traffic warm-up time in seconds, 0 = disable.
87     :param port_0: Port 0 on the traffic generator.
88     :param port_1: Port 1 on the traffic generator.
89     :param latency: With latency stats.
90     :param async_start: Start the traffic and exit.
91     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
92     :type profile_file: str
93     :type duration: float
94     :type framesize: int or str
95     :type mult: int
96     :type warmup_time: float
97     :type port_0: int
98     :type port_1: int
99     :type latency: bool
100     :type async_start: bool
101     :type traffic_directions: int
102     """
103     client = None
104     total_rcvd = 0
105     total_sent = 0
106     lost_a = 0
107     lost_b = 0
108     lat_a = u"-1/-1/-1/"
109     lat_b = u"-1/-1/-1/"
110     lat_a_hist = u""
111     lat_b_hist = u""
112     l7_data = u""
113     stats = dict()
114     stats_sampling = 1.0
115     approximated_duration = 0
116
117     # Read the profile.
118     try:
119         # TODO: key-values pairs to the profile file
120         #  - ips ?
121         print(f"### Profile file:\n{profile_file}")
122         profile = ASTFProfile.load(profile_file, framesize=framesize)
123     except TRexError:
124         print(f"Error while loading profile '{profile_file}'!")
125         raise
126
127     try:
128         # Create the client.
129         client = ASTFClient()
130         # Connect to server
131         client.connect()
132         # Acquire ports, stop the traffic, remove loaded traffic and clear
133         # stats.
134         client.reset()
135         # Load the profile.
136         client.load_profile(profile)
137
138         ports = [port_0]
139         if traffic_directions > 1:
140             ports.append(port_1)
141
142         # Warm-up phase.
143         if warmup_time > 0:
144             # Clear the stats before injecting.
145             client.clear_stats()
146             # Choose CPS and start traffic.
147             client.start(mult=mult, duration=warmup_time)
148             time_start = time.monotonic()
149
150             # Read the stats after the warmup duration (no sampling needed).
151             time.sleep(warmup_time)
152             stats[time.monotonic()-time_start] = client.get_stats()
153
154             if client.get_warnings():
155                 for warning in client.get_warnings():
156                     print(warning)
157
158             client.reset()
159
160             print(u"##### Warmup Statistics #####")
161             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
162
163             # TODO: check stats format
164             stats = stats[sorted(stats.keys())[-1]]
165             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
166             if traffic_directions > 1:
167                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
168
169             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
170             if traffic_directions > 1:
171                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
172
173         # Clear the stats before injecting.
174         lost_a = 0
175         lost_b = 0
176         stats = dict()
177
178         # Choose CPS and start traffic.
179         client.start(
180             mult=mult, duration=duration, nc=True,
181             latency_pps=mult if latency else 0, client_mask=2**len(ports)-1
182         )
183         time_start = time.monotonic()
184
185         if async_start:
186             # For async stop, we need to export the current snapshot.
187             xsnap0 = client.ports[port_0].get_xstats().reference_stats
188             print(f"Xstats snapshot 0: {xsnap0!r}")
189             if traffic_directions > 1:
190                 xsnap1 = client.ports[port_1].get_xstats().reference_stats
191                 print(f"Xstats snapshot 1: {xsnap1!r}")
192         else:
193             # Do not block until done.
194             while client.is_traffic_active(ports=ports):
195                 time.sleep(
196                     stats_sampling if stats_sampling < duration else duration
197                 )
198                 # Sample the stats.
199                 stats[time.monotonic()-time_start] = client.get_stats(
200                     ports=ports
201                 )
202             else:
203                 # Read the stats after the test
204                 stats[time.monotonic()-time_start] = client.get_stats(
205                     ports=ports
206                 )
207
208             if client.get_warnings():
209                 for warning in client.get_warnings():
210                     print(warning)
211
212             client.reset()
213
214             print(u"##### Statistics #####")
215             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
216
217             approximated_duration = list(sorted(stats.keys()))[-1]
218             stats = stats[sorted(stats.keys())[-1]]
219             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
220             if traffic_directions > 1:
221                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
222
223             # TODO: Latency measurement not used at this phase. This part will
224             #  be aligned in another commit.
225             # Stats index is not a port number, but "pgid".
226             if latency:
227                 lat_obj = stats[u"latency"][0][u"hist"]
228                 # TODO: Latency histogram is dictionary in astf mode,
229                 #  needs additional processing
230                 lat_a = fmt_latency(
231                     str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
232                     str(lat_obj[u"max_usec"]), u"-")
233                 lat_a_hist = str(lat_obj[u"histogram"])
234                 if traffic_directions > 1:
235                     lat_obj = stats[u"latency"][1][u"hist"]
236                     lat_b = fmt_latency(
237                         str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
238                         str(lat_obj[u"max_usec"]), u"-")
239                     lat_b_hist = str(lat_obj[u"histogram"])
240
241             if traffic_directions > 1:
242                 total_sent = \
243                     stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
244                 total_rcvd = \
245                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
246                 client_stats = stats[u"traffic"][u"client"]
247                 server_stats = stats[u"traffic"][u"server"]
248                 # Active and established flows UDP/TCP
249                 # Client
250                 c_act_flows = client_stats[u"m_active_flows"]
251                 c_est_flows = client_stats[u"m_est_flows"]
252                 l7_data = f"client_active_flows={c_act_flows}, "
253                 l7_data += f"client_established_flows={c_est_flows}, "
254                 # Server
255                 s_act_flows = server_stats[u"m_active_flows"]
256                 s_est_flows = server_stats[u"m_est_flows"]
257                 l7_data += f"server_active_flows={s_act_flows}, "
258                 l7_data += f"server_established_flows={s_est_flows}, "
259                 # Some zero counters are not sent
260                 if u"udp" in profile_file:
261                     # Client
262                     # Established connections
263                     c_udp_connects = client_stats.get(u"udps_connects", 0)
264                     l7_data += f"client_udp_connects={c_udp_connects}, "
265                     # Closed connections
266                     c_udp_closed = client_stats.get(u"udps_closed", 0)
267                     l7_data += f"client_udp_closed={c_udp_closed}, "
268                     # Server
269                     # Accepted connections
270                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
271                     l7_data += f"server_udp_accepts={s_udp_accepts}, "
272                     # Closed connections
273                     s_udp_closed = server_stats.get(u"udps_closed", 0)
274                     l7_data += f"server_udp_closed={s_udp_closed}, "
275                 elif u"tcp" in profile_file:
276                     # Client
277                     # Initiated connections
278                     c_tcp_connatt = client_stats.get(u"tcps_connattempt", 0)
279                     l7_data += f"client_tcp_connect_inits={c_tcp_connatt}, "
280                     # Established connections
281                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
282                     l7_data += f"client_tcp_connects={c_tcp_connects}, "
283                     # Closed connections
284                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
285                     l7_data += f"client_tcp_closed={c_tcp_closed}, "
286                     # Server
287                     # Accepted connections
288                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
289                     l7_data += f"server_tcp_accepts={s_tcp_accepts}, "
290                     # Established connections
291                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
292                     l7_data += f"server_tcp_connects={s_tcp_connects}, "
293                     # Closed connections
294                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
295                     l7_data += f"server_tcp_closed={s_tcp_closed}, "
296             else:
297                 total_sent = stats[port_0][u"opackets"]
298                 total_rcvd = stats[port_1][u"ipackets"]
299
300             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
301             if traffic_directions > 1:
302                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
303
304     except TRexError:
305         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
306         raise
307
308     finally:
309         if client:
310             if async_start:
311                 client.disconnect(stop_traffic=False, release_ports=True)
312             else:
313                 client.clear_profile()
314                 client.disconnect()
315                 print(
316                     f"cps={mult!r}, total_received={total_rcvd}, "
317                     f"total_sent={total_sent}, frame_loss={lost_a + lost_b}, "
318                     f"approximated_duration={approximated_duration}, "
319                     f"latency_stream_0(usec)={lat_a}, "
320                     f"latency_stream_1(usec)={lat_b}, "
321                     f"latency_hist_stream_0={lat_a_hist}, "
322                     f"latency_hist_stream_1={lat_b_hist}, "
323                     f"{l7_data}"
324                 )
325
326
327 def main():
328     """Main function for the traffic generator using T-rex.
329
330     It verifies the given command line arguments and runs "simple_burst"
331     function.
332     """
333     parser = argparse.ArgumentParser()
334     parser.add_argument(
335         u"-p", u"--profile", required=True, type=str,
336         help=u"Python traffic profile."
337     )
338     parser.add_argument(
339         u"-d", u"--duration", required=True, type=float,
340         help=u"Duration of traffic run."
341     )
342     parser.add_argument(
343         u"-s", u"--frame_size", required=True,
344         help=u"Size of a Frame without padding and IPG."
345     )
346     parser.add_argument(
347         u"-m", u"--mult", required=True, type=int,
348         help=u"Multiplier of profile CPS."
349     )
350     parser.add_argument(
351         u"-w", u"--warmup_time", type=float, default=5.0,
352         help=u"Traffic warm-up time in seconds, 0 = disable."
353     )
354     parser.add_argument(
355         u"--port_0", required=True, type=int,
356         help=u"Port 0 on the traffic generator."
357     )
358     parser.add_argument(
359         u"--port_1", required=True, type=int,
360         help=u"Port 1 on the traffic generator."
361     )
362     parser.add_argument(
363         u"--async_start", action=u"store_true", default=False,
364         help=u"Non-blocking call of the script."
365     )
366     parser.add_argument(
367         u"--latency", action=u"store_true", default=False,
368         help=u"Add latency stream."
369     )
370     parser.add_argument(
371         u"--traffic_directions", type=int, default=2,
372         help=u"Send bi- (2) or uni- (1) directional traffic."
373     )
374
375     args = parser.parse_args()
376
377     try:
378         framesize = int(args.frame_size)
379     except ValueError:
380         framesize = args.frame_size
381
382     simple_burst(
383         profile_file=args.profile, duration=args.duration, framesize=framesize,
384         mult=args.mult, warmup_time=args.warmup_time, port_0=args.port_0,
385         port_1=args.port_1, latency=args.latency, async_start=args.async_start,
386         traffic_directions=args.traffic_directions
387     )
388
389
390 if __name__ == u"__main__":
391     main()