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