T-Rex: 2.86
[csit.git] / GPL / tools / trex / trex_astf_profile.py
1 #!/usr/bin/python3
2
3 # Copyright (c) 2020 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 Apache 2.
18 #
19 # Unless required by applicable law or agreed to in writing, software
20 # distributed under the License is distributed on an "AS IS" BASIS,
21 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 # See the License for the specific language governing permissions and
23 # limitations under the License.
24
25 """This module gets T-Rex advanced stateful (astf) traffic profile together
26 with other parameters, reads the profile and sends the traffic. At the end, it
27 parses for various counters.
28 """
29
30 import argparse
31 import json
32 import sys
33 import time
34
35 sys.path.insert(
36     0, u"/opt/trex-core-2.86/scripts/automation/trex_control_plane/interactive/"
37 )
38 from trex.astf.api import *
39
40
41 def fmt_latency(lat_min, lat_avg, lat_max, hdrh):
42     """Return formatted, rounded latency.
43
44     :param lat_min: Min latency
45     :param lat_avg: Average latency
46     :param lat_max: Max latency
47     :param hdrh: Base64 encoded compressed HDRHistogram object.
48     :type lat_min: str
49     :type lat_avg: str
50     :type lat_max: str
51     :type hdrh: str
52     :return: Formatted and rounded output (hdrh unchanged) "min/avg/max/hdrh".
53     :rtype: str
54     """
55     try:
56         t_min = int(round(float(lat_min)))
57     except ValueError:
58         t_min = int(-1)
59     try:
60         t_avg = int(round(float(lat_avg)))
61     except ValueError:
62         t_avg = int(-1)
63     try:
64         t_max = int(round(float(lat_max)))
65     except ValueError:
66         t_max = int(-1)
67
68     return u"/".join(str(tmp) for tmp in (t_min, t_avg, t_max, hdrh))
69
70
71 def simple_burst(
72         profile_file,
73         duration,
74         framesize,
75         multiplier,
76         port_0,
77         port_1,
78         latency,
79         async_start=False,
80         traffic_directions=2,
81     ):
82     """Send traffic and measure packet loss and latency.
83
84     Procedure:
85      - reads the given traffic profile with streams,
86      - connects to the T-rex astf client,
87      - resets the ports,
88      - removes all existing streams,
89      - adds streams from the traffic profile to the ports,
90      - clears the statistics from the client,
91      - starts the traffic,
92      - waits for the defined time (or runs forever if async mode is defined),
93      - explicitly stops the traffic,
94      - reads and displays the statistics and
95      - disconnects from the client.
96
97     Duration details:
98     Contrary to stateless mode, ASTF profiles typically limit the number
99     of flows/transactions that can happen.
100     The caller is expected to set the duration parameter accordingly to
101     this limit and multiplier, including any overheads.
102     See *_traffic_duration output fields for TRex's measurement
103     of the real traffic duration (should be without any inactivity overheads).
104     If traffic has not ended by the final time, the traffic
105     is stopped explicitly, counters reflect the state just after the stop.
106
107     TODO: Support tests which focus only on some transaction phases,
108     e.g. TCP tests ignoring init and teardown separated by delays.
109     Currently, approximated time measures the whole traffic duration.
110
111     :param profile_file: A python module with T-rex traffic profile.
112     :param duration: Expected duration for all transactions to finish,
113         assuming only tolerable duration stretching happens.
114         This includes later start of later transactions
115         (according to TPS multiplier) and expected duration of each transaction.
116         Critically, this also includes any delay TRex shows when starting
117         traffic (but not the similar delay during stopping).
118     :param framesize: Frame size.
119     :param multiplier: Multiplier of profile CPS.
120     :param port_0: Port 0 on the traffic generator.
121     :param port_1: Port 1 on the traffic generator.
122     :param latency: With latency stats.
123     :param async_start: Start the traffic and exit.
124     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
125     :type profile_file: str
126     :type duration: float
127     :type framesize: int or str
128     :type multiplier: int
129     :type port_0: int
130     :type port_1: int
131     :type latency: bool
132     :type async_start: bool
133     :type traffic_directions: int
134     """
135     client = None
136     total_received = 0
137     total_sent = 0
138     lost_a = 0
139     lost_b = 0
140     lat_a = u"-1/-1/-1/"
141     lat_b = u"-1/-1/-1/"
142     lat_a_hist = u""
143     lat_b_hist = u""
144     l7_data = u""
145     stats = dict()
146     approximated_duration = 0
147
148     # Read the profile.
149     try:
150         # TODO: key-values pairs to the profile file
151         #  - ips ?
152         print(f"### Profile file:\n{profile_file}")
153         profile = ASTFProfile.load(profile_file, framesize=framesize)
154     except TRexError:
155         print(f"Error while loading profile '{profile_file}'!")
156         raise
157
158     try:
159         # Create the client.
160         client = ASTFClient()
161         # Connect to server
162         client.connect()
163         # Acquire ports, stop the traffic, remove loaded traffic and clear
164         # stats.
165         client.reset()
166         # Load the profile.
167         client.load_profile(profile)
168
169         ports = [port_0]
170         if traffic_directions > 1:
171             ports.append(port_1)
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=multiplier,
181             # Increase the input duration slightly,
182             # to ensure it does not end before sleep&stop below happens.
183             duration=duration + 0.1 if duration > 0 else duration,
184             nc=True,
185             latency_pps=int(multiplier) if latency else 0,
186             client_mask=2**len(ports)-1,
187         )
188         time_start = time.monotonic()
189
190         if async_start:
191             # For async stop, we need to export the current snapshot.
192             xsnap0 = client.ports[port_0].get_xstats().reference_stats
193             print(f"Xstats snapshot 0: {xsnap0!r}")
194             if traffic_directions > 1:
195                 xsnap1 = client.ports[port_1].get_xstats().reference_stats
196                 print(f"Xstats snapshot 1: {xsnap1!r}")
197         else:
198             time.sleep(duration)
199
200             # Do not block yet, the existing transactions may take long time
201             # to finish. We need an action that is almost reset(),
202             # but without clearing stats.
203             client.stop(block=False)
204             client.stop_latency()
205             client.remove_rx_queue(client.get_all_ports())
206             # Now we can wait for the real traffic stop.
207             client.stop(block=True)
208
209             # Read the stats after the traffic stopped (or time up).
210             stats[time.monotonic() - time_start] = client.get_stats(
211                 ports=ports
212             )
213
214             if client.get_warnings():
215                 for warning in client.get_warnings():
216                     print(warning)
217
218             # Now finish the complete reset.
219             client.reset()
220
221             print(u"##### Statistics #####")
222             print(json.dumps(stats, indent=4, separators=(u",", u": ")))
223
224             approximated_duration = list(sorted(stats.keys()))[-1]
225             stats = stats[sorted(stats.keys())[-1]]
226             lost_a = stats[port_0][u"opackets"] - stats[port_1][u"ipackets"]
227             if traffic_directions > 1:
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             if traffic_directions > 1:
249                 total_sent = \
250                     stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
251                 total_received = \
252                     stats[port_0][u"ipackets"] + stats[port_1][u"ipackets"]
253                 client_sent = stats[port_0][u"opackets"]
254                 client_received = stats[port_0][u"ipackets"]
255                 client_stats = stats[u"traffic"][u"client"]
256                 server_stats = stats[u"traffic"][u"server"]
257                 # Some zero counters are not sent
258                 # Active and established flows UDP/TCP
259                 # Client
260                 c_act_flows = client_stats[u"m_active_flows"]
261                 c_est_flows = client_stats[u"m_est_flows"]
262                 c_traffic_duration = client_stats.get(u"m_traffic_duration", 0)
263                 l7_data = f"client_active_flows={c_act_flows}; "
264                 l7_data += f"client_established_flows={c_est_flows}; "
265                 l7_data += f"client_traffic_duration={c_traffic_duration}; "
266                 # Possible errors
267                 # Too many packets in NIC rx queue
268                 c_err_rx_throttled = client_stats.get(u"err_rx_throttled", 0)
269                 l7_data += f"client_err_rx_throttled={c_err_rx_throttled}; "
270                 # Number of client side flows that were not opened
271                 # due to flow-table overflow
272                 c_err_nf_throttled = client_stats.get(u"err_c_nf_throttled", 0)
273                 l7_data += f"client_err_nf_throttled={c_err_nf_throttled}; "
274                 # Too many flows
275                 c_err_flow_overflow = client_stats.get(u"err_flow_overflow", 0)
276                 l7_data += f"client_err_flow_overflow={c_err_flow_overflow}; "
277                 # Server
278                 s_act_flows = server_stats[u"m_active_flows"]
279                 s_est_flows = server_stats[u"m_est_flows"]
280                 s_traffic_duration = server_stats.get(u"m_traffic_duration", 0)
281                 l7_data += f"server_active_flows={s_act_flows}; "
282                 l7_data += f"server_established_flows={s_est_flows}; "
283                 l7_data += f"server_traffic_duration={s_traffic_duration}; "
284                 # Possible errors
285                 # Too many packets in NIC rx queue
286                 s_err_rx_throttled = server_stats.get(u"err_rx_throttled", 0)
287                 l7_data += f"client_err_rx_throttled={s_err_rx_throttled}; "
288                 if u"udp" in profile_file:
289                     # Client
290                     # Established connections
291                     c_udp_connects = client_stats.get(u"udps_connects", 0)
292                     l7_data += f"client_udp_connects={c_udp_connects}; "
293                     # Closed connections
294                     c_udp_closed = client_stats.get(u"udps_closed", 0)
295                     l7_data += f"client_udp_closed={c_udp_closed}; "
296                     # Sent bytes
297                     c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
298                     l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
299                     # Sent packets
300                     c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
301                     l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
302                     # Received bytes
303                     c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
304                     l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
305                     # Received packets
306                     c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
307                     l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
308                     # Keep alive drops
309                     c_udp_keepdrops = client_stats.get(u"udps_keepdrops", 0)
310                     l7_data += f"client_udp_keep_drops={c_udp_keepdrops}; "
311                     # Client without flow
312                     c_err_cwf = client_stats.get(u"err_cwf", 0)
313                     l7_data += f"client_err_cwf={c_err_cwf}; "
314                     # Server
315                     # Accepted connections
316                     s_udp_accepts = server_stats.get(u"udps_accepts", 0)
317                     l7_data += f"server_udp_accepts={s_udp_accepts}; "
318                     # Closed connections
319                     s_udp_closed = server_stats.get(u"udps_closed", 0)
320                     l7_data += f"server_udp_closed={s_udp_closed}; "
321                     # Sent bytes
322                     s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
323                     l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
324                     # Sent packets
325                     s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
326                     l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
327                     # Received bytes
328                     s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
329                     l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
330                     # Received packets
331                     s_udp_rcvpkt = server_stats.get(u"udps_rcvpkt", 0)
332                     l7_data += f"server_udp_rx_packets={s_udp_rcvpkt}; "
333                 elif u"tcp" in profile_file:
334                     # Client
335                     # Connection attempts
336                     c_tcp_connattempt = client_stats.get(u"tcps_connattempt", 0)
337                     l7_data += f"client_tcp_connattempt={c_tcp_connattempt}; "
338                     # Established connections
339                     c_tcp_connects = client_stats.get(u"tcps_connects", 0)
340                     l7_data += f"client_tcp_connects={c_tcp_connects}; "
341                     # Closed connections
342                     c_tcp_closed = client_stats.get(u"tcps_closed", 0)
343                     l7_data += f"client_tcp_closed={c_tcp_closed}; "
344                     # Send bytes
345                     c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
346                     l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
347                     # Received bytes
348                     c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
349                     l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
350                     # Server
351                     # Accepted connections
352                     s_tcp_accepts = server_stats.get(u"tcps_accepts", 0)
353                     l7_data += f"server_tcp_accepts={s_tcp_accepts}; "
354                     # Established connections
355                     s_tcp_connects = server_stats.get(u"tcps_connects", 0)
356                     l7_data += f"server_tcp_connects={s_tcp_connects}; "
357                     # Closed connections
358                     s_tcp_closed = server_stats.get(u"tcps_closed", 0)
359                     l7_data += f"server_tcp_closed={s_tcp_closed}; "
360                     # Sent bytes
361                     s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
362                     l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
363                     # Received bytes
364                     s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
365                     l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
366             else:
367                 total_sent = stats[port_0][u"opackets"]
368                 total_received = stats[port_1][u"ipackets"]
369
370             print(f"packets lost from {port_0} --> {port_1}: {lost_a} pkts")
371             if traffic_directions > 1:
372                 print(f"packets lost from {port_1} --> {port_0}: {lost_b} pkts")
373
374     except TRexError:
375         print(u"T-Rex ASTF runtime error!", file=sys.stderr)
376         raise
377
378     finally:
379         if client:
380             if async_start:
381                 client.disconnect(stop_traffic=False, release_ports=True)
382             else:
383                 client.clear_profile()
384                 client.disconnect()
385                 print(
386                     f"multiplier={multiplier!r}; "
387                     f"total_received={total_received}; "
388                     f"total_sent={total_sent}; "
389                     f"frame_loss={lost_a + lost_b}; "
390                     f"approximated_duration={approximated_duration}; "
391                     f"latency_stream_0(usec)={lat_a}; "
392                     f"latency_stream_1(usec)={lat_b}; "
393                     f"latency_hist_stream_0={lat_a_hist}; "
394                     f"latency_hist_stream_1={lat_b_hist}; "
395                     f"client_sent={client_sent}; "
396                     f"client_received={client_received}; "
397                     f"{l7_data}"
398                 )
399
400
401 def main():
402     """Main function for the traffic generator using T-rex.
403
404     It verifies the given command line arguments and runs "simple_burst"
405     function.
406     """
407     parser = argparse.ArgumentParser()
408     parser.add_argument(
409         u"-p", u"--profile", required=True, type=str,
410         help=u"Python traffic profile."
411     )
412     parser.add_argument(
413         u"-d", u"--duration", required=True, type=float,
414         help=u"Duration of the whole traffic run, including overheads."
415     )
416     parser.add_argument(
417         u"-s", u"--frame_size", required=True,
418         help=u"Size of a Frame without padding and IPG."
419     )
420     parser.add_argument(
421         u"-m", u"--multiplier", required=True, type=float,
422         help=u"Multiplier of profile CPS."
423     )
424     parser.add_argument(
425         u"--port_0", required=True, type=int,
426         help=u"Port 0 on the traffic generator."
427     )
428     parser.add_argument(
429         u"--port_1", required=True, type=int,
430         help=u"Port 1 on the traffic generator."
431     )
432     parser.add_argument(
433         u"--async_start", action=u"store_true", default=False,
434         help=u"Non-blocking call of the script."
435     )
436     parser.add_argument(
437         u"--latency", action=u"store_true", default=False,
438         help=u"Add latency stream."
439     )
440     parser.add_argument(
441         u"--traffic_directions", type=int, default=2,
442         help=u"Send bi- (2) or uni- (1) directional traffic."
443     )
444
445     args = parser.parse_args()
446
447     try:
448         framesize = int(args.frame_size)
449     except ValueError:
450         framesize = args.frame_size
451
452     simple_burst(
453         profile_file=args.profile,
454         duration=args.duration,
455         framesize=framesize,
456         multiplier=args.multiplier,
457         port_0=args.port_0,
458         port_1=args.port_1,
459         latency=args.latency,
460         async_start=args.async_start,
461         traffic_directions=args.traffic_directions,
462     )
463
464
465 if __name__ == u"__main__":
466     main()