blob: 29c8a1a22661058dbb8c35701697b6b8941d8bfe [file] [log] [blame]
Daniele Moro4a5a91f2021-09-07 17:24:39 +02001"""
2Copyright 2021 Open Networking Foundation (ONF)
3
4Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
5the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
6or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
7
8"""
9import time
10import os
11import collections
12import numpy as np
13
14from drivers.common.api.controllerdriver import Controller
15from trex.stl.api import STLClient, STLStreamDstMAC_PKT
16from trex_stf_lib.trex_client import CTRexClient
17from trex_stl_lib.api import STLFlowLatencyStats, STLPktBuilder, STLStream, \
18 STLTXCont
19
20from socket import error as ConnectionRefusedError
21from distutils.util import strtobool
22
23TREX_FILES_DIR = "/tmp/trex_files/"
24
25LatencyStats = collections.namedtuple(
26 "LatencyStats",
27 [
28 "pg_id",
29 "jitter",
30 "average",
31 "total_max",
32 "total_min",
33 "last_max",
34 "histogram",
35 "dropped",
36 "out_of_order",
37 "duplicate",
38 "seq_too_high",
39 "seq_too_low",
40 "percentile_50",
41 "percentile_75",
42 "percentile_90",
43 "percentile_99",
44 "percentile_99_9",
45 "percentile_99_99",
46 "percentile_99_999",
47 ],
48)
49
50PortStats = collections.namedtuple(
51 "PortStats",
52 [
53 "tx_packets",
54 "rx_packets",
55 "tx_bytes",
56 "rx_bytes",
57 "tx_errors",
58 "rx_errors",
59 "tx_bps",
60 "tx_pps",
61 "tx_bps_L1",
62 "tx_util",
63 "rx_bps",
64 "rx_pps",
65 "rx_bps_L1",
66 "rx_util",
67 ],
68)
69
70FlowStats = collections.namedtuple(
71 "FlowStats",
72 [
73 "pg_id",
74 "tx_packets",
75 "rx_packets",
76 "tx_bytes",
77 "rx_bytes",
78 ],
79)
80
81
82class TrexClientDriver(Controller):
83 """
84 Implements a Trex Client Driver
85 """
86
87 def __init__(self):
88 self.trex_address = "localhost"
89 self.trex_config = None # Relative path in dependencies of the test using this driver
90 self.force_restart = True
91 self.sofware_mode = False
92 self.setup_successful = False
93 self.stats = None
94 self.trex_client = None
95 self.trex_daemon_client = None
96 super(TrexClientDriver, self).__init__()
97
98 def connect(self, **connectargs):
99 try:
100 for key in connectargs:
101 vars(self)[key] = connectargs[key]
102 for key in self.options:
103 if key == "trex_address":
104 self.trex_address = self.options[key]
105 elif key == "trex_config":
106 self.trex_config = self.options[key]
107 elif key == "force_restart":
108 self.force_restart = bool(strtobool(self.options[key]))
109 elif key == "software_mode":
110 self.software_mode = bool(strtobool(self.options[key]))
111 self.name = self.options["name"]
112 except Exception as inst:
113 main.log.error("Uncaught exception: " + str(inst))
114 main.cleanAndExit()
115 return super(TrexClientDriver, self).connect()
116
117 def disconnect(self):
118 """
119 Called when Test is complete
120 """
121 self.disconnectTrexClient()
122 self.stopTrexServer()
123 return main.TRUE
124
125 def setupTrex(self, pathToTrexConfig):
126 """
127 Setup TRex server passing the TRex configuration.
128 :return: True if setup successful, False otherwise
129 """
130 main.log.debug(self.name + ": Setting up TRex server")
131 if self.software_mode:
132 trex_args = "--software --no-hw-flow-stat"
133 else:
134 trex_args = None
135 self.trex_daemon_client = CTRexClient(self.trex_address,
136 trex_args=trex_args)
137 success = self.__set_up_trex_server(
138 self.trex_daemon_client, self.trex_address,
139 pathToTrexConfig + self.trex_config,
140 self.force_restart
141 )
142 if not success:
143 main.log.error("Failed to set up TRex daemon!")
144 return False
145 self.setup_successful = True
146 return True
147
148 def connectTrexClient(self):
149 if not self.setup_successful:
150 main.log.error("Cannot connect TRex Client, first setup TRex")
151 return False
152 main.log.info("Connecting TRex Client")
153 self.trex_client = STLClient(server=self.trex_address)
154 self.trex_client.connect()
155 self.trex_client.acquire()
156 self.trex_client.reset() # Resets configs from all ports
157 self.trex_client.clear_stats() # Clear status from all ports
158 # Put all ports to promiscuous mode, otherwise they will drop all
159 # incoming packets if the destination mac is not the port mac address.
160 self.trex_client.set_port_attr(self.trex_client.get_all_ports(),
161 promiscuous=True)
162 # Reset the used sender ports
163 self.all_sender_port = set()
164 self.stats = None
165 return True
166
167 def disconnectTrexClient(self):
168 # Teardown TREX Client
169 if self.trex_client is not None:
170 main.log.info("Tearing down STLClient...")
171 self.trex_client.stop()
172 self.trex_client.release()
173 self.trex_client.disconnect()
174 self.trex_client = None
175 # Do not reset stats
176
177 def stopTrexServer(self):
178 if self.trex_daemon_client is not None:
179 self.trex_daemon_client.stop_trex()
180 self.trex_daemon_client = None
181
182 def addStream(self, pkt, trex_port, l1_bps=None, percentage=None,
183 delay=0, flow_id=None, flow_stats=False):
184 """
185 :param pkt: Scapy packet, TRex will send copy of this packet
186 :param trex_port: Port number to send packet from, must match a port in the TRex config file
187 :param l1_bps: L1 Throughput generated by TRex (mutually exclusive with percentage)
188 :param percentage: Percentage usage of the selected port bandwidth (mutually exlusive with l1_bps)
189 :param flow_id: Flow ID, required when saving latency statistics
190 :param flow_stats: True to measure flow statistics (latency and packet), False otherwise, might require software mode
191 :return: True if the stream is create, false otherwise
192 """
193 if (percentage is None and l1_bps is None) or (
194 percentage is not None and l1_bps is not None):
195 main.log.error(
196 "Either percentage or l1_bps must be provided when creating a stream")
197 return False
198 main.log.debug("Creating flow stream")
199 main.log.debug(
200 "port: %d, l1_bps: %s, percentage: %s, delay: %d, flow_id:%s, flow_stats: %s" % (
201 trex_port, str(l1_bps), str(percentage), delay, str(flow_id),
202 str(flow_stats)))
203 main.log.debug(pkt.summary())
204 if flow_stats:
205 traffic_stream = self.__create_latency_stats_stream(
206 pkt,
207 pg_id=flow_id,
208 isg=delay,
209 percentage=percentage,
210 l1_bps=l1_bps)
211 else:
212 traffic_stream = self.__create_background_stream(
213 pkt,
214 percentage=percentage,
215 l1_bps=l1_bps)
216 self.trex_client.add_streams(traffic_stream, ports=trex_port)
217 self.all_sender_port.add(trex_port)
218 return True
219
220 def startAndWaitTraffic(self, duration=10):
221 """
222 Start generating traffic and wait traffic to be send
223 :param duration: Traffic generation duration
224 :return:
225 """
226 if not self.trex_client:
227 main.log.error(
228 "Cannot start traffic, first connect the TRex client")
229 return False
230 main.log.info("Start sending traffic for %d seconds" % duration)
231 self.trex_client.start(list(self.all_sender_port), mult="1",
232 duration=duration)
233 main.log.info("Waiting until all traffic is sent..")
234 self.trex_client.wait_on_traffic(ports=list(self.all_sender_port),
235 rx_delay_ms=100)
236 main.log.info("...traffic sent!")
237 # Reset sender port so we can run other tests with the same TRex client
238 self.all_sender_port = set()
239 main.log.info("Getting stats")
240 self.stats = self.trex_client.get_stats()
241 main.log.info("GOT stats")
242
243 def getFlowStats(self, flow_id):
244 if self.stats is None:
245 main.log.error("No stats saved!")
246 return None
247 return TrexClientDriver.__get_flow_stats(flow_id, self.stats)
248
249 def logFlowStats(self, flow_id):
250 main.log.info("Statistics for flow {}: {}".format(
251 flow_id,
252 TrexClientDriver.__get_readable_flow_stats(
253 self.getFlowStats(flow_id))))
254
255 def getLatencyStats(self, flow_id):
256 if self.stats is None:
257 main.log.error("No stats saved!")
258 return None
259 return TrexClientDriver.__get_latency_stats(flow_id, self.stats)
260
261 def logLatencyStats(self, flow_id):
262 main.log.info("Latency statistics for flow {}: {}".format(
263 flow_id,
264 TrexClientDriver.__get_readable_latency_stats(
265 self.getLatencyStats(flow_id))))
266
267 def getPortStats(self, port_id):
268 if self.stats is None:
269 main.log.error("No stats saved!")
270 return None
271 return TrexClientDriver.__get_port_stats(port_id, self.stats)
272
273 def logPortStats(self, port_id):
274 if self.stats is None:
275 main.log.error("No stats saved!")
276 return None
277 main.log.info("Statistics for port {}: {}".format(
278 port_id, TrexClientDriver.__get_readable_port_stats(
279 self.stats.get(port_id))))
280
281 # From ptf/test/common/ptf_runner.py
282 def __set_up_trex_server(self, trex_daemon_client, trex_address,
283 trex_config,
284 force_restart):
285 try:
286 main.log.info("Pushing Trex config %s to the server" % trex_config)
287 if not trex_daemon_client.push_files(trex_config):
288 main.log.error("Unable to push %s to Trex server" % trex_config)
289 return False
290
291 if force_restart:
292 main.log.info("Restarting TRex")
293 trex_daemon_client.kill_all_trexes()
294 time.sleep(1)
295
296 if not trex_daemon_client.is_idle():
297 main.log.info("The Trex server process is running")
298 main.log.warn(
299 "A Trex server process is still running, "
300 + "use --force-restart to kill it if necessary."
301 )
302 return False
303
304 trex_config_file_on_server = TREX_FILES_DIR + os.path.basename(
305 trex_config)
306 trex_daemon_client.start_stateless(cfg=trex_config_file_on_server)
307 except ConnectionRefusedError:
308 main.log.error(
309 "Unable to connect to server %s.\n" + "Did you start the Trex daemon?" % trex_address)
310 return False
311
312 return True
313
314 def __create_latency_stats_stream(self, pkt, pg_id,
315 name=None,
316 l1_bps=None,
317 percentage=None,
318 isg=0):
319 assert (percentage is None and l1_bps is not None) or (
320 percentage is not None and l1_bps is None)
321 return STLStream(
322 name=name,
323 packet=STLPktBuilder(pkt=pkt),
324 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage),
325 isg=isg,
326 flow_stats=STLFlowLatencyStats(pg_id=pg_id)
327 )
328
329 def __create_background_stream(self, pkt, name=None, percentage=None,
330 l1_bps=None):
331 assert (percentage is None and l1_bps is not None) or (
332 percentage is not None and l1_bps is None)
333 return STLStream(
334 name=name,
335 packet=STLPktBuilder(pkt=pkt),
336 mode=STLTXCont(bps_L1=l1_bps, percentage=percentage)
337 )
338
339 # Multiplier for data rates
340 K = 1000
341 M = 1000 * K
342 G = 1000 * M
343
344 @staticmethod
345 def __to_readable(src, unit="bps"):
346 """
347 Convert number to human readable string.
348 For example: 1,000,000 bps to 1Mbps. 1,000 bytes to 1KB
349
350 :parameters:
351 src : int
352 the original data
353 unit : str
354 the unit ('bps', 'pps', or 'bytes')
355 :returns:
356 A human readable string
357 """
358 if src < 1000:
359 return "{:.1f} {}".format(src, unit)
360 elif src < 1000000:
361 return "{:.1f} K{}".format(src / 1000, unit)
362 elif src < 1000000000:
363 return "{:.1f} M{}".format(src / 1000000, unit)
364 else:
365 return "{:.1f} G{}".format(src / 1000000000, unit)
366
367 @staticmethod
368 def __get_readable_port_stats(port_stats):
369 opackets = port_stats.get("opackets", 0)
370 ipackets = port_stats.get("ipackets", 0)
371 obytes = port_stats.get("obytes", 0)
372 ibytes = port_stats.get("ibytes", 0)
373 oerrors = port_stats.get("oerrors", 0)
374 ierrors = port_stats.get("ierrors", 0)
375 tx_bps = port_stats.get("tx_bps", 0)
376 tx_pps = port_stats.get("tx_pps", 0)
377 tx_bps_L1 = port_stats.get("tx_bps_L1", 0)
378 tx_util = port_stats.get("tx_util", 0)
379 rx_bps = port_stats.get("rx_bps", 0)
380 rx_pps = port_stats.get("rx_pps", 0)
381 rx_bps_L1 = port_stats.get("rx_bps_L1", 0)
382 rx_util = port_stats.get("rx_util", 0)
383 return """
384 Output packets: {}
385 Input packets: {}
386 Output bytes: {} ({})
387 Input bytes: {} ({})
388 Output errors: {}
389 Input errors: {}
390 TX bps: {} ({})
391 TX pps: {} ({})
392 L1 TX bps: {} ({})
393 TX util: {}
394 RX bps: {} ({})
395 RX pps: {} ({})
396 L1 RX bps: {} ({})
397 RX util: {}""".format(
398 opackets,
399 ipackets,
400 obytes,
401 TrexClientDriver.__to_readable(obytes, "Bytes"),
402 ibytes,
403 TrexClientDriver.__to_readable(ibytes, "Bytes"),
404 oerrors,
405 ierrors,
406 tx_bps,
407 TrexClientDriver.__to_readable(tx_bps),
408 tx_pps,
409 TrexClientDriver.__to_readable(tx_pps, "pps"),
410 tx_bps_L1,
411 TrexClientDriver.__to_readable(tx_bps_L1),
412 tx_util,
413 rx_bps,
414 TrexClientDriver.__to_readable(rx_bps),
415 rx_pps,
416 TrexClientDriver.__to_readable(rx_pps, "pps"),
417 rx_bps_L1,
418 TrexClientDriver.__to_readable(rx_bps_L1),
419 rx_util,
420 )
421
422 @staticmethod
423 def __get_port_stats(port, stats):
424 """
425 :param port: int
426 :param stats:
427 :return:
428 """
429 port_stats = stats.get(port)
430 return PortStats(
431 tx_packets=port_stats.get("opackets", 0),
432 rx_packets=port_stats.get("ipackets", 0),
433 tx_bytes=port_stats.get("obytes", 0),
434 rx_bytes=port_stats.get("ibytes", 0),
435 tx_errors=port_stats.get("oerrors", 0),
436 rx_errors=port_stats.get("ierrors", 0),
437 tx_bps=port_stats.get("tx_bps", 0),
438 tx_pps=port_stats.get("tx_pps", 0),
439 tx_bps_L1=port_stats.get("tx_bps_L1", 0),
440 tx_util=port_stats.get("tx_util", 0),
441 rx_bps=port_stats.get("rx_bps", 0),
442 rx_pps=port_stats.get("rx_pps", 0),
443 rx_bps_L1=port_stats.get("rx_bps_L1", 0),
444 rx_util=port_stats.get("rx_util", 0),
445 )
446
447 @staticmethod
448 def __get_latency_stats(pg_id, stats):
449 """
450 :param pg_id: int
451 :param stats:
452 :return:
453 """
454
455 lat_stats = stats["latency"].get(pg_id)
456 lat = lat_stats["latency"]
457 # Estimate latency percentiles from the histogram.
458 l = list(lat["histogram"].keys())
459 l.sort()
460 all_latencies = []
461 for sample in l:
462 range_start = sample
463 if range_start == 0:
464 range_end = 10
465 else:
466 range_end = range_start + pow(10, (len(str(range_start)) - 1))
467 val = lat["histogram"][sample]
468 # Assume whole the bucket experienced the range_end latency.
469 all_latencies += [range_end] * val
470 q = [50, 75, 90, 99, 99.9, 99.99, 99.999]
471 percentiles = np.percentile(all_latencies, q)
472
473 ret = LatencyStats(
474 pg_id=pg_id,
475 jitter=lat["jitter"],
476 average=lat["average"],
477 total_max=lat["total_max"],
478 total_min=lat["total_min"],
479 last_max=lat["last_max"],
480 histogram=lat["histogram"],
481 dropped=lat_stats["err_cntrs"]["dropped"],
482 out_of_order=lat_stats["err_cntrs"]["out_of_order"],
483 duplicate=lat_stats["err_cntrs"]["dup"],
484 seq_too_high=lat_stats["err_cntrs"]["seq_too_high"],
485 seq_too_low=lat_stats["err_cntrs"]["seq_too_low"],
486 percentile_50=percentiles[0],
487 percentile_75=percentiles[1],
488 percentile_90=percentiles[2],
489 percentile_99=percentiles[3],
490 percentile_99_9=percentiles[4],
491 percentile_99_99=percentiles[5],
492 percentile_99_999=percentiles[6],
493 )
494 return ret
495
496 @staticmethod
497 def __get_readable_latency_stats(stats):
498 """
499 :param stats: LatencyStats
500 :return:
501 """
502 histogram = ""
503 # need to listify in order to be able to sort them.
504 l = list(stats.histogram.keys())
505 l.sort()
506 for sample in l:
507 range_start = sample
508 if range_start == 0:
509 range_end = 10
510 else:
511 range_end = range_start + pow(10, (len(str(range_start)) - 1))
512 val = stats.histogram[sample]
513 histogram = (
514 histogram
515 + "\n Packets with latency between {0:>5} us and {1:>5} us: {2:>10}".format(
516 range_start, range_end, val
517 )
518 )
519
520 return """
521 Latency info for pg_id {}
522 Dropped packets: {}
523 Out-of-order packets: {}
524 Sequence too high packets: {}
525 Sequence too low packets: {}
526 Maximum latency: {} us
527 Minimum latency: {} us
528 Maximum latency in last sampling period: {} us
529 Average latency: {} us
530 50th percentile latency: {} us
531 75th percentile latency: {} us
532 90th percentile latency: {} us
533 99th percentile latency: {} us
534 99.9th percentile latency: {} us
535 99.99th percentile latency: {} us
536 99.999th percentile latency: {} us
537 Jitter: {} us
538 Latency distribution histogram: {}
539 """.format(stats.pg_id, stats.dropped, stats.out_of_order,
540 stats.seq_too_high, stats.seq_too_low, stats.total_max,
541 stats.total_min, stats.last_max, stats.average,
542 stats.percentile_50, stats.percentile_75,
543 stats.percentile_90,
544 stats.percentile_99, stats.percentile_99_9,
545 stats.percentile_99_99,
546 stats.percentile_99_999, stats.jitter, histogram)
547
548 @staticmethod
549 def __get_flow_stats(pg_id, stats):
550 """
551 :param pg_id: int
552 :param stats:
553 :return:
554 """
555 FlowStats = collections.namedtuple(
556 "FlowStats",
557 ["pg_id", "tx_packets", "rx_packets", "tx_bytes", "rx_bytes", ],
558 )
559 flow_stats = stats["flow_stats"].get(pg_id)
560 ret = FlowStats(
561 pg_id=pg_id,
562 tx_packets=flow_stats["tx_pkts"]["total"],
563 rx_packets=flow_stats["rx_pkts"]["total"],
564 tx_bytes=flow_stats["tx_bytes"]["total"],
565 rx_bytes=flow_stats["rx_bytes"]["total"],
566 )
567 return ret
568
569 @staticmethod
570 def __get_readable_flow_stats(stats):
571 """
572 :param stats: FlowStats
573 :return:
574 """
575 return """Flow info for pg_id {}
576 TX packets: {}
577 RX packets: {}
578 TX bytes: {}
579 RX bytes: {}""".format(stats.pg_id, stats.tx_packets,
580 stats.rx_packets, stats.tx_bytes,
581 stats.rx_bytes)