3 # Copyright (c) 2021 Cisco and/or its affiliates.
5 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
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:
12 # http://www.apache.org/licenses/LICENSE-2.0
13 # https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
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
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.
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.
37 0, u"/opt/trex-core-2.88/scripts/automation/trex_control_plane/interactive/"
39 from trex.astf.api import ASTFClient, ASTFProfile, TRexError
42 def fmt_latency(lat_min, lat_avg, lat_max, hdrh):
43 """Return formatted, rounded latency.
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.
53 :return: Formatted and rounded output (hdrh unchanged) "min/avg/max/hdrh".
57 t_min = int(round(float(lat_min)))
61 t_avg = int(round(float(lat_avg)))
65 t_max = int(round(float(lat_max)))
69 return u"/".join(str(tmp) for tmp in (t_min, t_avg, t_max, hdrh))
84 """Send traffic and measure packet loss and latency.
87 - reads the given traffic profile with streams,
88 - connects to the T-rex astf client,
90 - removes all existing streams,
91 - adds streams from the traffic profile to the ports,
92 - clears the statistics from the client,
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.
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.
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.
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
132 :type async_start: bool
133 :type traffic_directions: int
147 approximated_duration = 0
151 # TODO: key-values pairs to the profile file
153 print(f"### Profile file:\n{profile_file}")
154 profile = ASTFProfile.load(profile_file, framesize=framesize)
156 print(f"Error while loading profile '{profile_file}'!")
161 client = ASTFClient()
164 # Acquire ports, stop the traffic, remove loaded traffic and clear
168 client.load_profile(profile)
171 if traffic_directions > 1:
174 # Clear the stats before injecting.
179 # Choose CPS and start traffic.
184 latency_pps=int(multiplier) if latency else 0,
185 client_mask=2**len(ports)-1,
187 time_stop = time.monotonic() + duration + delay
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}")
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)
207 # Read the stats after the traffic stopped (or time up).
208 stats[time.monotonic() - time_stop] = client.get_stats(
212 if client.get_warnings():
213 for warning in client.get_warnings():
216 # Now finish the complete reset.
219 print(u"##### Statistics #####")
220 print(json.dumps(stats, indent=4, separators=(u",", u": ")))
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"]
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".
232 lat_obj = stats[u"latency"][0][u"hist"]
233 # TODO: Latency histogram is dictionary in astf mode,
234 # needs additional processing
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"]
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"])
246 if traffic_directions > 1:
248 stats[port_0][u"opackets"] + stats[port_1][u"opackets"]
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
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}; "
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}; "
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}; "
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}; "
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:
288 # Established connections
289 c_udp_connects = client_stats.get(u"udps_connects", 0)
290 l7_data += f"client_udp_connects={c_udp_connects}; "
292 c_udp_closed = client_stats.get(u"udps_closed", 0)
293 l7_data += f"client_udp_closed={c_udp_closed}; "
295 c_udp_sndbyte = client_stats.get(u"udps_sndbyte", 0)
296 l7_data += f"client_udp_tx_bytes={c_udp_sndbyte}; "
298 c_udp_sndpkt = client_stats.get(u"udps_sndpkt", 0)
299 l7_data += f"client_udp_tx_packets={c_udp_sndpkt}; "
301 c_udp_rcvbyte = client_stats.get(u"udps_rcvbyte", 0)
302 l7_data += f"client_udp_rx_bytes={c_udp_rcvbyte}; "
304 c_udp_rcvpkt = client_stats.get(u"udps_rcvpkt", 0)
305 l7_data += f"client_udp_rx_packets={c_udp_rcvpkt}; "
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}; "
313 # Accepted connections
314 s_udp_accepts = server_stats.get(u"udps_accepts", 0)
315 l7_data += f"server_udp_accepts={s_udp_accepts}; "
317 s_udp_closed = server_stats.get(u"udps_closed", 0)
318 l7_data += f"server_udp_closed={s_udp_closed}; "
320 s_udp_sndbyte = server_stats.get(u"udps_sndbyte", 0)
321 l7_data += f"server_udp_tx_bytes={s_udp_sndbyte}; "
323 s_udp_sndpkt = server_stats.get(u"udps_sndpkt", 0)
324 l7_data += f"server_udp_tx_packets={s_udp_sndpkt}; "
326 s_udp_rcvbyte = server_stats.get(u"udps_rcvbyte", 0)
327 l7_data += f"server_udp_rx_bytes={s_udp_rcvbyte}; "
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:
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}; "
340 c_tcp_closed = client_stats.get(u"tcps_closed", 0)
341 l7_data += f"client_tcp_closed={c_tcp_closed}; "
343 c_tcp_sndbyte = client_stats.get(u"tcps_sndbyte", 0)
344 l7_data += f"client_tcp_tx_bytes={c_tcp_sndbyte}; "
346 c_tcp_rcvbyte = client_stats.get(u"tcps_rcvbyte", 0)
347 l7_data += f"client_tcp_rx_bytes={c_tcp_rcvbyte}; "
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}; "
356 s_tcp_closed = server_stats.get(u"tcps_closed", 0)
357 l7_data += f"server_tcp_closed={s_tcp_closed}; "
359 s_tcp_sndbyte = server_stats.get(u"tcps_sndbyte", 0)
360 l7_data += f"server_tcp_tx_bytes={s_tcp_sndbyte}; "
362 s_tcp_rcvbyte = server_stats.get(u"tcps_rcvbyte", 0)
363 l7_data += f"server_tcp_rx_bytes={s_tcp_rcvbyte}; "
365 total_sent = stats[port_0][u"opackets"]
366 total_received = stats[port_1][u"ipackets"]
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")
373 print(u"T-Rex ASTF runtime error!", file=sys.stderr)
379 client.disconnect(stop_traffic=False, release_ports=True)
381 client.clear_profile()
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}; "
400 """Main function for the traffic generator using T-rex.
402 It verifies the given command line arguments and runs "simple_burst"
405 parser = argparse.ArgumentParser()
407 u"-p", u"--profile", required=True, type=str,
408 help=u"Python traffic profile."
411 u"-d", u"--duration", required=True, type=float,
412 help=u"Duration of the whole traffic run, including overheads."
415 u"-s", u"--frame_size", required=True,
416 help=u"Size of a Frame without padding and IPG."
419 u"-m", u"--multiplier", required=True, type=float,
420 help=u"Multiplier of profile CPS."
423 u"--port_0", required=True, type=int,
424 help=u"Port 0 on the traffic generator."
427 u"--port_1", required=True, type=int,
428 help=u"Port 1 on the traffic generator."
431 u"--async_start", action=u"store_true", default=False,
432 help=u"Non-blocking call of the script."
435 u"--latency", action=u"store_true", default=False,
436 help=u"Add latency stream."
439 u"--traffic_directions", type=int, default=2,
440 help=u"Send bi- (2) or uni- (1) directional traffic."
443 u"--delay", required=True, type=float, default=0.0,
444 help=u"Allowed time overhead, sleep time is increased by this [s]."
447 args = parser.parse_args()
450 framesize = int(args.frame_size)
452 framesize = args.frame_size
455 profile_file=args.profile,
456 duration=args.duration,
458 multiplier=args.multiplier,
461 latency=args.latency,
462 async_start=args.async_start,
463 traffic_directions=args.traffic_directions,
468 if __name__ == u"__main__":