fix(trex): use reset instead of clear_profile
[csit.git] / GPL / tools / trex / trex_astf_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 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-2.97/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]
178         if traffic_directions > 1:
179             ports.append(port_1)
180
181         # Clear the stats before injecting.
182         lost_a = 0
183         lost_b = 0
184         stats = dict()
185
186         # Choose CPS and start traffic.
187         client.start(
188             mult=multiplier,
189             duration=duration,
190             nc=True,
191             latency_pps=int(multiplier) if latency else 0,
192             client_mask=2**len(ports)-1,
193         )
194         time_stop = time.monotonic() + duration + delay
195
196         if async_start:
197             # For async stop, we need to export the current snapshot.
198             xsnap0 = client.ports[port_0].get_xstats().reference_stats
199             print(f"Xstats snapshot 0: {xsnap0!r}")
200             if traffic_directions > 1:
201                 xsnap1 = client.ports[port_1].get_xstats().reference_stats
202                 print(f"Xstats snapshot 1: {xsnap1!r}")
203         else:
204             time.sleep(duration + delay)
205             # Do not block yet, the existing transactions may take long time
206             # to finish. We need an action that is almost reset(),
207             # but without clearing stats.
208             client.stop(block=False)
209             client.stop_latency()
210             client.remove_rx_queue(client.get_all_ports())
211             # Now we can wait for the real traffic stop.
212             client.stop(block=True)
213
214             # Read the stats after the traffic stopped (or time up).
215             stats[time.monotonic() - time_stop] = client.get_stats(
216                 ports=ports
217             )
218
219             if client.get_warnings():
220                 for warning in client.get_warnings():
221                     print(warning)
222
223             # No profile cleanup here, reset will be done in the finally block.
224
225             print(u"##### Statistics #####")
226             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
227
228             approximated_duration = list(sorted(stats.keys()))[-1]
229             stats = stats[sorted(stats.keys())[-1]]
230             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
231             if traffic_directions > 1:
232                 lost_b = stats[port_1][u"opackets"] - stats[port_0][u"ipackets"]
233
234             # TODO: Latency measurement not used at this phase. This part will
235             #  be aligned in another commit.
236             # Stats index is not a port number, but "pgid".
237             if latency:
238                 lat_obj = stats[u"latency"][0][u"hist"]
239                 # TODO: Latency histogram is dictionary in astf mode,
240                 #  needs additional processing
241                 lat_a = fmt_latency(
242                     str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
243                     str(lat_obj[u"max_usec"]), u"-")
244                 lat_a_hist = str(lat_obj[u"histogram"])
245                 if traffic_directions > 1:
246                     lat_obj = stats[u"latency"][1][u"hist"]
247                     lat_b = fmt_latency(
248                         str(lat_obj[u"min_usec"]), str(lat_obj[u"s_avg"]),
249                         str(lat_obj[u"max_usec"]), u"-")
250                     lat_b_hist = str(lat_obj[u"histogram"])
251
252             if traffic_directions > 1:
253                 total_sent = \
254                     stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
255                 total_received = \
256                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
257                 client_sent = stats[port_0][u"opackets"]
258                 client_received = stats[port_0][u"ipackets"]
259                 client_stats = stats[u"traffic"][u"client"]
260                 server_stats = stats[u"traffic"][u"server"]
261                 # Some zero counters are not sent
262                 # Active and established flows UDP/TCP
263                 # Client
264                 c_act_flows = client_stats[u"m_active_flows"]
265                 c_est_flows = client_stats[u"m_est_flows"]
266                 c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
267                 l7_data = f"client_active_flows={c_act_flows}; "
268                 l7_data += f"client_established_flows={c_est_flows}; "
269                 l7_data += f"client_traffic_duration={c_traffic_duration}; "
270                 # Possible errors
271                 # Too many packets in NIC rx queue
272                 c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
273                 l7_data += f"client_err_rx_throttled={c_err_rx_throttled}; "
274                 # Number of client side flows that were not opened
275                 # due to flow-table overflow
276                 c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
277                 l7_data += f"client_err_nf_throttled={c_err_nf_throttled}; "
278                 # Too many flows
279                 c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
280                 l7_data += f"client_err_flow_overflow={c_err_flow_overflow}; "
281                 # Server
282                 s_act_flows = server_stats[u"m_active_flows"]
283                 s_est_flows = server_stats[u"m_est_flows"]
284                 s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
285                 l7_data += f"server_active_flows={s_act_flows}; "
286                 l7_data += f"server_established_flows={s_est_flows}; "
287                 l7_data += f"server_traffic_duration={s_traffic_duration}; "
288                 # Possible errors
289                 # Too many packets in NIC rx queue
290                 s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
291                 l7_data += f"client_err_rx_throttled={s_err_rx_throttled}; "
292                 if u"udp" in profile_file:
293                     # Client
294                     # Established connections
295                     c_udp_connects = client_stats.get(u"udps_connects", 0)
296                     l7_data += f"client_udp_connects={c_udp_connects}; "
297                     # Closed connections
298                     c_udp_closed = client_stats.get(u"udps_closed", 0)
299                     l7_data += f"client_udp_closed={c_udp_closed}; "
300                     # Sent bytes
301                     c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
302                     l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
303                     # Sent packets
304                     c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
305                     l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
306                     # Received bytes
307                     c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
308                     l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
309                     # Received packets
310                     c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
311                     l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
312                     # Keep alive drops
313                     c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
314                     l7_data += f"client_udp_keep_drops={c_udp_keepdrops}; "
315                     # Client without flow
316                     c_err_cwf = client_stats.get(u"err_cwf", 0)
317                     l7_data += f"client_err_cwf={c_err_cwf}; "
318                     # Server
319                     # Accepted connections
320                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
321                     l7_data += f"server_udp_accepts={s_udp_accepts}; "
322                     # Closed connections
323                     s_udp_closed = server_stats.get(u"udps_closed", 0)
324                     l7_data += f"server_udp_closed={s_udp_closed}; "
325                     # Sent bytes
326                     s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
327                     l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
328                     # Sent packets
329                     s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
330                     l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
331                     # Received bytes
332                     s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
333                     l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
334                     # Received packets
335                     s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
336                     l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}; "
337                 elif u"tcp" in profile_file:
338                     # Client
339                     # Connection attempts
340                     c_tcp_connattempt = client_stats.get(u"tcps_connattempt", 0)
341                     l7_data += f"client_tcp_connattempt={c_tcp_connattempt}; "
342                     # Established connections
343                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
344                     l7_data += f"client_tcp_connects={c_tcp_connects}; "
345                     # Closed connections
346                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
347                     l7_data += f"client_tcp_closed={c_tcp_closed}; "
348                     # Send bytes
349                     c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
350                     l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
351                     # Received bytes
352                     c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
353                     l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
354                     # Server
355                     # Accepted connections
356                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
357                     l7_data += f"server_tcp_accepts={s_tcp_accepts}; "
358                     # Established connections
359                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
360                     l7_data += f"server_tcp_connects={s_tcp_connects}; "
361                     # Closed connections
362                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
363                     l7_data += f"server_tcp_closed={s_tcp_closed}; "
364                     # Sent bytes
365                     s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
366                     l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
367                     # Received bytes
368                     s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
369                     l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
370             else:
371                 total_sent = stats[port_0][u"opackets"]
372                 total_received = stats[port_1][u"ipackets"]
373
374             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
375             if traffic_directions > 1:
376                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
377
378     except TRexError:
379         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
380         raise
381
382     finally:
383         if client:
384             if async_start:
385                 client.disconnect(stop_traffic=False, release_ports=True)
386             else:
387                 client.reset()
388                 client.disconnect()
389                 print(
390                     f"multiplier={multiplier!r}; "
391                     f"total_received={total_received}; "
392                     f"total_sent={total_sent}; "
393                     f"frame_loss={lost_a + lost_b}; "
394                     f"approximated_duration={approximated_duration}; "
395                     f"latency_stream_0(usec)={lat_a}; "
396                     f"latency_stream_1(usec)={lat_b}; "
397                     f"latency_hist_stream_0={lat_a_hist}; "
398                     f"latency_hist_stream_1={lat_b_hist}; "
399                     f"client_sent={client_sent}; "
400                     f"client_received={client_received}; "
401                     f"{l7_data}"
402                 )
403
404
405 def main():
406     """Main function for the traffic generator using T-rex.
407
408     It verifies the given command line arguments and runs "simple_burst"
409     function.
410     """
411     parser = argparse.ArgumentParser()
412     parser.add_argument(
413         u"-p", u"--profile", required=True, type=str,
414         help=u"Python traffic profile."
415     )
416     parser.add_argument(
417         u"-d", u"--duration", required=True, type=float,
418         help=u"Duration of the whole traffic run, including overheads."
419     )
420     parser.add_argument(
421         u"-s", u"--frame_size", required=True,
422         help=u"Size of a Frame without padding and IPG."
423     )
424     parser.add_argument(
425         u"--n_data_frames", type=int, default=5,
426         help=u"Use this many data frames per transaction and direction (TPUT)."
427     )
428     parser.add_argument(
429         u"-m", u"--multiplier", required=True, type=float,
430         help=u"Multiplier of profile CPS."
431     )
432     parser.add_argument(
433         u"--port_0", required=True, type=int,
434         help=u"Port 0 on the traffic generator."
435     )
436     parser.add_argument(
437         u"--port_1", required=True, type=int,
438         help=u"Port 1 on the traffic generator."
439     )
440     parser.add_argument(
441         u"--async_start", action=u"store_true", default=False,
442         help=u"Non-blocking call of the script."
443     )
444     parser.add_argument(
445         u"--latency", action=u"store_true", default=False,
446         help=u"Add latency stream."
447     )
448     parser.add_argument(
449         u"--traffic_directions", type=int, default=2,
450         help=u"Send bi- (2) or uni- (1) directional traffic."
451     )
452     parser.add_argument(
453         u"--delay", required=True, type=float, default=0.0,
454         help=u"Allowed time overhead, sleep time is increased by this [s]."
455     )
456
457     args = parser.parse_args()
458
459     try:
460         framesize = int(args.frame_size)
461     except ValueError:
462         framesize = args.frame_size
463
464     simple_burst(
465         profile_file=args.profile,
466         duration=args.duration,
467         framesize=framesize,
468         n_data_frames=args.n_data_frames,
469         multiplier=args.multiplier,
470         port_0=args.port_0,
471         port_1=args.port_1,
472         latency=args.latency,
473         async_start=args.async_start,
474         traffic_directions=args.traffic_directions,
475         delay=args.delay,
476     )
477
478
479 if __name__ == u"__main__":
480     main()