Revert "fix(IPsecUtil): Delete keywords no longer used"
[csit.git] / GPL / tools / trex / trex_astf_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2023 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 T-Rex advanced stateful (astf) traffic profile together
27 with other parameters, reads the profile and sends the traffic. At the end, it
28 parses for various counters.
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-3.03/scripts/automation/trex_control_plane/interactive/"
38 )
39 from trex.astf.api import ASTFClient, ASTFProfile, TRexError
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         n_data_frames,
77         multiplier,
78         port_0,
79         port_1,
80         latency,
81         async_start=False,
82         traffic_directions=2,
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 astf client,
90      - resets the ports,
91      - removes all existing streams,
92      - adds streams from the traffic profile to the ports,
93      - clears the statistics from the client,
94      - starts the traffic,
95      - waits for the defined time (or runs forever if async mode is defined),
96      - explicitly stops the traffic,
97      - reads and displays the statistics and
98      - disconnects from the client.
99
100     Duration details:
101     Contrary to stateless mode, ASTF profiles typically limit the number
102     of flows/transactions that can happen.
103     The caller is expected to set the duration parameter to idealized value,
104     but set the delay arguments when TRex is expected
105     to finish processing replies later (including a window for latency).
106     See *_traffic_duration output fields for TRex's measurement
107     of the real traffic duration (should be without any inactivity overheads).
108     If traffic has not ended by the final time, the traffic
109     is stopped explicitly, counters reflect the state just after the stop.
110
111     TODO: Support tests which focus only on some transaction phases,
112     e.g. TCP tests ignoring init and teardown separated by delays.
113     Currently, approximated time measures the whole traffic duration.
114
115     :param profile_file: A python module with T-rex traffic profile.
116     :param duration: Expected duration for all transactions to finish,
117         without any TRex related delays, without even latency.
118     :param framesize: Frame size.
119     :param n_data_frames: Controls "size" of transaction for TPUT tests.
120     :param multiplier: Multiplier of profile CPS.
121     :param port_0: Port 0 on the traffic generator.
122     :param port_1: Port 1 on the traffic generator.
123     :param latency: With latency stats.
124     :param async_start: Start the traffic and exit.
125     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
126     :param delay: Time increase [s] for sleep duration.
127     :type profile_file: str
128     :type duration: float
129     :type framesize: int or str
130     :type n_data_frames: int
131     :type multiplier: int
132     :type port_0: int
133     :type port_1: int
134     :type latency: bool
135     :type async_start: bool
136     :type traffic_directions: int
137     :type delay: float
138     """
139     client = None
140     total_received = 0
141     total_sent = 0
142     lost_a = 0
143     lost_b = 0
144     lat_a = u"-1/-1/-1/"
145     lat_b = u"-1/-1/-1/"
146     lat_a_hist = u""
147     lat_b_hist = u""
148     l7_data = u""
149     stats = dict()
150     approximated_duration = 0
151
152     # Read the profile.
153     try:
154         # TODO: key-values pairs to the profile file
155         #  - ips ?
156         print(f"### Profile file:\n{profile_file}")
157         profile = ASTFProfile.load(
158             profile_file,
159             framesize=framesize,
160             n_data_frames=n_data_frames,
161         )
162     except TRexError:
163         print(f"Error while loading profile '{profile_file}'!")
164         raise
165
166     try:
167         # Create the client.
168         client = ASTFClient()
169         # Connect to server
170         client.connect()
171         # Acquire ports, stop the traffic, remove loaded traffic and clear
172         # stats.
173         client.reset()
174         # Load the profile.
175         client.load_profile(profile)
176
177         ports = [port_0, port_1]
178
179         # Clear the stats before injecting.
180         lost_a = 0
181         lost_b = 0
182         stats = dict()
183
184         # Choose CPS and start traffic.
185         client.start(
186             mult=multiplier,
187             duration=duration,
188             nc=True,
189             latency_pps=int(multiplier) if latency else 0,
190             client_mask=2**len(ports)-1,
191         )
192         time_stop = time.monotonic() + duration + delay
193
194         if async_start:
195             # For async stop, we need to export the current snapshot.
196             xsnap0 = client.ports[port_0].get_xstats().reference_stats
197             print(f"Xstats snapshot 0: {xsnap0!r}")
198             xsnap1 = client.ports[port_1].get_xstats().reference_stats
199             print(f"Xstats snapshot 1: {xsnap1!r}")
200         else:
201             time.sleep(duration + delay)
202             # Do not block yet, the existing transactions may take long time
203             # to finish. We need an action that is almost reset(),
204             # but without clearing stats.
205             client.stop(block=False)
206             client.stop_latency()
207             client.remove_rx_queue(client.get_all_ports())
208             # Now we can wait for the real traffic stop.
209             client.stop(block=True)
210
211             # Read the stats after the traffic stopped (or time up).
212             stats[time.monotonic() - time_stop] = client.get_stats(
213                 ports=ports
214             )
215
216             if client.get_warnings():
217                 for warning in client.get_warnings():
218                     print(warning)
219
220             # No profile cleanup here, reset will be done in the finally block.
221
222             print(u"##### Statistics #####")
223             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
224
225             approximated_duration = duration + list(sorted(stats.keys()))[-1]
226             stats = stats[sorted(stats.keys())[-1]]
227             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
228             lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
229
230             # TODO: Latency measurement not used at this phase. This part will
231             #  be aligned in another commit.
232             # Stats index is not a port number, but "pgid".
233             if latency:
234                 lat_obj = stats[u"latency"][0][u"hist"]
235                 # TODO: Latency histogram is dictionary in astf mode,
236                 #  needs additional processing
237                 lat_a = fmt_latency(
238                     str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
239                     str(lat_obj[u"max_usec"]), u"-")
240                 lat_a_hist = str(lat_obj[u"histogram"])
241                 if traffic_directions > 1:
242                     lat_obj = stats[u"latency"][1][u"hist"]
243                     lat_b = fmt_latency(
244                         str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
245                         str(lat_obj[u"max_usec"]), u"-")
246                     lat_b_hist = str(lat_obj[u"histogram"])
247
248             total_sent = \
249                 stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
250             total_received = \
251                 stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
252             client_sent = stats[port_0][u"opackets"]
253             client_received = stats[port_0][u"ipackets"]
254             client_stats = stats[u"traffic"][u"client"]
255             server_stats = stats[u"traffic"][u"server"]
256             # Some zero counters are not sent
257             # Active and established flows UDP/TCP
258             # Client
259             c_act_flows = client_stats[u"m_active_flows"]
260             c_est_flows = client_stats[u"m_est_flows"]
261             c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
262             l7_data = f"client_active_flows={c_act_flows}; "
263             l7_data += f"client_established_flows={c_est_flows}; "
264             l7_data += f"client_traffic_duration={c_traffic_duration}; "
265             # Possible errors
266             # Too many packets in NIC rx queue
267             c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
268             l7_data += f"client_err_rx_throttled={c_err_rx_throttled}; "
269             # Number of client side flows that were not opened
270             # due to flow-table overflow
271             c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
272             l7_data += f"client_err_nf_throttled={c_err_nf_throttled}; "
273             # Too many flows
274             c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
275             l7_data += f"client_err_flow_overflow={c_err_flow_overflow}; "
276             # Server
277             s_act_flows = server_stats[u"m_active_flows"]
278             s_est_flows = server_stats[u"m_est_flows"]
279             s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
280             l7_data += f"server_active_flows={s_act_flows}; "
281             l7_data += f"server_established_flows={s_est_flows}; "
282             l7_data += f"server_traffic_duration={s_traffic_duration}; "
283             # Possible errors
284             # Too many packets in NIC rx queue
285             s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
286             l7_data += f"client_err_rx_throttled={s_err_rx_throttled}; "
287             if u"udp" in profile_file:
288                 # Client
289                 # Established connections
290                 c_udp_connects = client_stats.get(u"udps_connects", 0)
291                 l7_data += f"client_udp_connects={c_udp_connects}; "
292                 # Closed connections
293                 c_udp_closed = client_stats.get(u"udps_closed", 0)
294                 l7_data += f"client_udp_closed={c_udp_closed}; "
295                 # Sent bytes
296                 c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
297                 l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
298                 # Sent packets
299                 c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
300                 l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
301                 # Received bytes
302                 c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
303                 l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
304                 # Received packets
305                 c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
306                 l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
307                 # Keep alive drops
308                 c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
309                 l7_data += f"client_udp_keep_drops={c_udp_keepdrops}; "
310                 # Client without flow
311                 c_err_cwf = client_stats.get(u"err_cwf", 0)
312                 l7_data += f"client_err_cwf={c_err_cwf}; "
313                 # Server
314                 # Accepted connections
315                 s_udp_accepts = server_stats.get(u"udps_accepts", 0)
316                 l7_data += f"server_udp_accepts={s_udp_accepts}; "
317                 # Closed connections
318                 s_udp_closed = server_stats.get(u"udps_closed", 0)
319                 l7_data += f"server_udp_closed={s_udp_closed}; "
320                 # Sent bytes
321                 s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
322                 l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
323                 # Sent packets
324                 s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
325                 l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
326                 # Received bytes
327                 s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
328                 l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
329                 # Received packets
330                 s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
331                 l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}; "
332             elif u"tcp" in profile_file:
333                 # Client
334                 # Connection attempts
335                 c_tcp_connattempt = client_stats.get(u"tcps_connattempt", 0)
336                 l7_data += f"client_tcp_connattempt={c_tcp_connattempt}; "
337                 # Established connections
338                 c_tcp_connects = client_stats.get(u"tcps_connects", 0)
339                 l7_data += f"client_tcp_connects={c_tcp_connects}; "
340                 # Closed connections
341                 c_tcp_closed = client_stats.get(u"tcps_closed", 0)
342                 l7_data += f"client_tcp_closed={c_tcp_closed}; "
343                 # Send bytes
344                 c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
345                 l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
346                 # Received bytes
347                 c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
348                 l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
349                 # Server
350                 # Accepted connections
351                 s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
352                 l7_data += f"server_tcp_accepts={s_tcp_accepts}; "
353                 # Established connections
354                 s_tcp_connects = server_stats.get(u"tcps_connects", 0)
355                 l7_data += f"server_tcp_connects={s_tcp_connects}; "
356                 # Closed connections
357                 s_tcp_closed = server_stats.get(u"tcps_closed", 0)
358                 l7_data += f"server_tcp_closed={s_tcp_closed}; "
359                 # Sent bytes
360                 s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
361                 l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
362                 # Received bytes
363                 s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
364                 l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
365
366             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
367             print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
368
369     except TRexError:
370         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
371         raise
372
373     finally:
374         if client:
375             if async_start:
376                 client.disconnect(stop_traffic=False, release_ports=True)
377             else:
378                 client.reset()
379                 client.disconnect()
380                 print(
381                     f"multiplier={multiplier!r}; "
382                     f"total_received={total_received}; "
383                     f"total_sent={total_sent}; "
384                     f"frame_loss={lost_a + lost_b}; "
385                     f"approximated_duration={approximated_duration}; "
386                     f"latency_stream_0(usec)={lat_a}; "
387                     f"latency_stream_1(usec)={lat_b}; "
388                     f"latency_hist_stream_0={lat_a_hist}; "
389                     f"latency_hist_stream_1={lat_b_hist}; "
390                     f"client_sent={client_sent}; "
391                     f"client_received={client_received}; "
392                     f"{l7_data}"
393                 )
394
395
396 def main():
397     """Main function for the traffic generator using T-rex.
398
399     It verifies the given command line arguments and runs "simple_burst"
400     function.
401     """
402     parser = argparse.ArgumentParser()
403     parser.add_argument(
404         u"-p", u"--profile", required=True, type=str,
405         help=u"Python traffic profile."
406     )
407     parser.add_argument(
408         u"-d", u"--duration", required=True, type=float,
409         help=u"Duration of the whole traffic run, including overheads."
410     )
411     parser.add_argument(
412         u"-s", u"--frame_size", required=True,
413         help=u"Size of a Frame without padding and IPG."
414     )
415     parser.add_argument(
416         u"--n_data_frames", type=int, default=5,
417         help=u"Use this many data frames per transaction and direction (TPUT)."
418     )
419     parser.add_argument(
420         u"-m", u"--multiplier", required=True, type=float,
421         help=u"Multiplier of profile CPS."
422     )
423     parser.add_argument(
424         u"--port_0", required=True, type=int,
425         help=u"Port 0 on the traffic generator."
426     )
427     parser.add_argument(
428         u"--port_1", required=True, type=int,
429         help=u"Port 1 on the traffic generator."
430     )
431     parser.add_argument(
432         u"--async_start", action=u"store_true", default=False,
433         help=u"Non-blocking call of the script."
434     )
435     parser.add_argument(
436         u"--latency", action=u"store_true", default=False,
437         help=u"Add latency stream."
438     )
439     parser.add_argument(
440         u"--traffic_directions", type=int, default=2,
441         help=u"Send bi- (2) or uni- (1) directional traffic."
442     )
443     parser.add_argument(
444         u"--delay", required=True, type=float, default=0.0,
445         help=u"Allowed time overhead, sleep time is increased by this [s]."
446     )
447
448     args = parser.parse_args()
449
450     try:
451         framesize = int(args.frame_size)
452     except ValueError:
453         framesize = args.frame_size
454
455     simple_burst(
456         profile_file=args.profile,
457         duration=args.duration,
458         framesize=framesize,
459         n_data_frames=args.n_data_frames,
460         multiplier=args.multiplier,
461         port_0=args.port_0,
462         port_1=args.port_1,
463         latency=args.latency,
464         async_start=args.async_start,
465         traffic_directions=args.traffic_directions,
466         delay=args.delay,
467     )
468
469
470 if __name__ == u"__main__":
471     main()