Separate files needing GPL license
[csit.git] / resources / libraries / python / TrafficGenerator.py
1 # Copyright (c) 2020 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Performance testing traffic generator library."""
15
16 import time
17
18 from robot.api import logger
19 from robot.libraries.BuiltIn import BuiltIn
20
21 from .Constants import Constants
22 from .CpuUtils import CpuUtils
23 from .DropRateSearch import DropRateSearch
24 from .MLRsearch.AbstractMeasurer import AbstractMeasurer
25 from .MLRsearch.MultipleLossRatioSearch import MultipleLossRatioSearch
26 from .MLRsearch.ReceiveRateMeasurement import ReceiveRateMeasurement
27 from .PLRsearch.PLRsearch import PLRsearch
28 from .OptionString import OptionString
29 from .ssh import exec_cmd_no_error, exec_cmd
30 from .topology import NodeType
31 from .topology import NodeSubTypeTG
32 from .topology import Topology
33
34 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
35
36
37 def check_subtype(node):
38     """Return supported subtype of given node, or raise an exception.
39
40     Currently only one subtype is supported,
41     but we want our code to be ready for other ones.
42
43     :param node: Topology node to check. Can be None.
44     :type node: dict or NoneType
45     :returns: Subtype detected.
46     :rtype: NodeSubTypeTG
47     :raises RuntimeError: If node is not supported, message explains how.
48     """
49     if node.get(u"type") is None:
50         msg = u"Node type is not defined"
51     elif node['type'] != NodeType.TG:
52         msg = f"Node type is {node[u'type']!r}, not a TG"
53     elif node.get(u"subtype") is None:
54         msg = u"TG subtype is not defined"
55     elif node[u"subtype"] != NodeSubTypeTG.TREX:
56         msg = f"TG subtype {node[u'subtype']!r} is not supported"
57     else:
58         return NodeSubTypeTG.TREX
59     raise RuntimeError(msg)
60
61
62 class TGDropRateSearchImpl(DropRateSearch):
63     """Drop Rate Search implementation."""
64
65     # def __init__(self):
66     #     super(TGDropRateSearchImpl, self).__init__()
67
68     def measure_loss(
69             self, rate, frame_size, loss_acceptance, loss_acceptance_type,
70             traffic_profile, skip_warmup=False):
71         """Runs the traffic and evaluate the measured results.
72
73         :param rate: Offered traffic load.
74         :param frame_size: Size of frame.
75         :param loss_acceptance: Permitted drop ratio or frames count.
76         :param loss_acceptance_type: Type of permitted loss.
77         :param traffic_profile: Module name as a traffic profile identifier.
78             See GPL/traffic_profiles/trex for implemented modules.
79         :param skip_warmup: Start TRex without warmup traffic if true.
80         :type rate: float
81         :type frame_size: str
82         :type loss_acceptance: float
83         :type loss_acceptance_type: LossAcceptanceType
84         :type traffic_profile: str
85         :type skip_warmup: bool
86         :returns: Drop threshold exceeded? (True/False)
87         :rtype: bool
88         :raises NotImplementedError: If TG is not supported.
89         :raises RuntimeError: If TG is not specified.
90         """
91         # we need instance of TrafficGenerator instantiated by Robot Framework
92         # to be able to use trex_stl-*()
93         tg_instance = BuiltIn().get_library_instance(
94             u"resources.libraries.python.TrafficGenerator"
95         )
96         subtype = check_subtype(tg_instance.node)
97         if subtype == NodeSubTypeTG.TREX:
98             unit_rate = str(rate) + self.get_rate_type_str()
99             if skip_warmup:
100                 tg_instance.trex_stl_start_remote_exec(
101                     self.get_duration(), unit_rate, frame_size, traffic_profile,
102                     warmup_time=0.0
103                 )
104             else:
105                 tg_instance.trex_stl_start_remote_exec(
106                     self.get_duration(), unit_rate, frame_size, traffic_profile
107                 )
108             loss = tg_instance.get_loss()
109             sent = tg_instance.get_sent()
110             if self.loss_acceptance_type_is_percentage():
111                 loss = (float(loss) / float(sent)) * 100
112             logger.trace(
113                 f"comparing: {loss} < {loss_acceptance} {loss_acceptance_type}"
114             )
115             return float(loss) <= float(loss_acceptance)
116         return False
117
118     def get_latency(self):
119         """Returns min/avg/max latency.
120
121         :returns: Latency stats.
122         :rtype: list
123         """
124         tg_instance = BuiltIn().get_library_instance(
125             u"resources.libraries.python.TrafficGenerator"
126         )
127         return tg_instance.get_latency_int()
128
129
130 # TODO: Pylint says too-many-instance-attributes.
131 # A fix is developed in https://gerrit.fd.io/r/c/csit/+/22221
132 class TrafficGenerator(AbstractMeasurer):
133     """Traffic Generator.
134
135     FIXME: Describe API."""
136
137     # TODO: Decrease friction between various search and rate provider APIs.
138     # TODO: Remove "trex" from lines which could work with other TGs.
139
140     # Use one instance of TrafficGenerator for all tests in test suite
141     ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
142
143     def __init__(self):
144         # TODO: Number of fields will be reduced with CSIT-1378.
145         self._node = None
146         # T-REX interface order mapping
147         self._ifaces_reordered = False
148         # Result holding fields, to be removed.
149         self._result = None
150         self._loss = None
151         self._sent = None
152         self._latency = None
153         self._received = None
154         self._approximated_rate = None
155         self._approximated_duration = None
156         # Measurement input fields, needed for async stop result.
157         self._start_time = None
158         self._rate = None
159         # Other input parameters, not knowable from measure() signature.
160         self.frame_size = None
161         self.traffic_profile = None
162         self.warmup_time = None
163         self.traffic_directions = None
164         # Transient data needed for async measurements.
165         self._xstats = (None, None)
166         # TODO: Rename "xstats" to something opaque, so TRex is not privileged?
167
168     @property
169     def node(self):
170         """Getter.
171
172         :returns: Traffic generator node.
173         :rtype: dict
174         """
175         return self._node
176
177     def get_loss(self):
178         """Return number of lost packets.
179
180         :returns: Number of lost packets.
181         :rtype: str
182         """
183         return self._loss
184
185     def get_sent(self):
186         """Return number of sent packets.
187
188         :returns: Number of sent packets.
189         :rtype: str
190         """
191         return self._sent
192
193     def get_received(self):
194         """Return number of received packets.
195
196         :returns: Number of received packets.
197         :rtype: str
198         """
199         return self._received
200
201     def get_latency_int(self):
202         """Return rounded min/avg/max latency.
203
204         :returns: Latency stats.
205         :rtype: list
206         """
207         return self._latency
208
209     def get_approximated_rate(self):
210         """Return approximated rate computed as ratio of transmited packets over
211         duration of trial.
212
213         :returns: Approximated rate.
214         :rtype: str
215         """
216         return self._approximated_rate
217
218     # TODO: pylint says disable=too-many-locals.
219     # A fix is developed in https://gerrit.fd.io/r/c/csit/+/22221
220     def initialize_traffic_generator(
221             self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
222             tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
223             tg_if2_dst_mac=None):
224         """TG initialization.
225
226         TODO: Document why do we need (and how do we use) _ifaces_reordered.
227
228         :param tg_node: Traffic generator node.
229         :param tg_if1: TG - name of first interface.
230         :param tg_if2: TG - name of second interface.
231         :param tg_if1_adj_node: TG if1 adjecent node.
232         :param tg_if1_adj_if: TG if1 adjecent interface.
233         :param tg_if2_adj_node: TG if2 adjecent node.
234         :param tg_if2_adj_if: TG if2 adjecent interface.
235         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
236         :param tg_if1_dst_mac: Interface 1 destination MAC address.
237         :param tg_if2_dst_mac: Interface 2 destination MAC address.
238         :type tg_node: dict
239         :type tg_if1: str
240         :type tg_if2: str
241         :type tg_if1_adj_node: dict
242         :type tg_if1_adj_if: str
243         :type tg_if2_adj_node: dict
244         :type tg_if2_adj_if: str
245         :type osi_layer: str
246         :type tg_if1_dst_mac: str
247         :type tg_if2_dst_mac: str
248         :returns: nothing
249         :raises RuntimeError: In case of issue during initialization.
250         """
251         subtype = check_subtype(tg_node)
252         if subtype == NodeSubTypeTG.TREX:
253             self._node = tg_node
254
255             if1_pci = Topology().get_interface_pci_addr(self._node, tg_if1)
256             if2_pci = Topology().get_interface_pci_addr(self._node, tg_if2)
257             if1_addr = Topology().get_interface_mac(self._node, tg_if1)
258             if2_addr = Topology().get_interface_mac(self._node, tg_if2)
259
260             if osi_layer == u"L2":
261                 if1_adj_addr = if2_addr
262                 if2_adj_addr = if1_addr
263             elif osi_layer == u"L3":
264                 if1_adj_addr = Topology().get_interface_mac(
265                     tg_if1_adj_node, tg_if1_adj_if
266                 )
267                 if2_adj_addr = Topology().get_interface_mac(
268                     tg_if2_adj_node, tg_if2_adj_if
269                 )
270             elif osi_layer == u"L7":
271                 if1_addr = Topology().get_interface_ip4(self._node, tg_if1)
272                 if2_addr = Topology().get_interface_ip4(self._node, tg_if2)
273                 if1_adj_addr = Topology().get_interface_ip4(
274                     tg_if1_adj_node, tg_if1_adj_if
275                 )
276                 if2_adj_addr = Topology().get_interface_ip4(
277                     tg_if2_adj_node, tg_if2_adj_if
278                 )
279             else:
280                 raise ValueError(u"Unknown Test Type")
281
282             # in case of switched environment we can override MAC addresses
283             if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
284                 if1_adj_addr = tg_if1_dst_mac
285                 if2_adj_addr = tg_if2_dst_mac
286
287             if min(if1_pci, if2_pci) != if1_pci:
288                 if1_pci, if2_pci = if2_pci, if1_pci
289                 if1_addr, if2_addr = if2_addr, if1_addr
290                 if1_adj_addr, if2_adj_addr = if2_adj_addr, if1_adj_addr
291                 self._ifaces_reordered = True
292
293             master_thread_id, latency_thread_id, socket, threads = \
294                 CpuUtils.get_affinity_trex(
295                     self._node, tg_if1, tg_if2,
296                     tg_dtc=Constants.TREX_CORE_COUNT)
297
298             if osi_layer in (u"L2", u"L3"):
299                 dst_mac0 = f"0x{if1_adj_addr.replace(u':', u',0x')}"
300                 src_mac0 = f"0x{if1_addr.replace(u':', u',0x')}"
301                 dst_mac1 = f"0x{if2_adj_addr.replace(u':', u',0x')}"
302                 src_mac1 = f"0x{if2_addr.replace(u':', u',0x')}"
303                 exec_cmd_no_error(
304                     self._node,
305                     f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
306                     f"- version: 2\n"
307                     f"  c: {len(threads)}\n"
308                     f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
309                     f"  interfaces: [\"{if1_pci}\",\"{if2_pci}\"]\n"
310                     f"  port_info:\n"
311                     f"      - dest_mac: [{dst_mac0}]\n"
312                     f"        src_mac: [{src_mac0}]\n"
313                     f"      - dest_mac: [{dst_mac1}]\n"
314                     f"        src_mac: [{src_mac1}]\n"
315                     f"  platform :\n"
316                     f"      master_thread_id: {master_thread_id}\n"
317                     f"      latency_thread_id: {latency_thread_id}\n"
318                     f"      dual_if:\n"
319                     f"          - socket: {socket}\n"
320                     f"            threads: {threads}\n"
321                     f"EOF'",
322                     sudo=True, message=u"TRex config generation!"
323                 )
324             elif osi_layer == u"L7":
325                 exec_cmd_no_error(
326                     self._node,
327                     f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
328                     f"- version: 2\n"
329                     f"  c: {len(threads)}\n"
330                     f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
331                     f"  interfaces: [\"{if1_pci}\",\"{if2_pci}\"]\n"
332                     f"  port_info:\n"
333                     f"      - ip: [{if1_addr}]\n"
334                     f"        default_gw: [{if1_adj_addr}]\n"
335                     f"      - ip: [{if2_addr}]\n"
336                     f"        default_gw: [{if2_adj_addr}]\n"
337                     f"  platform :\n"
338                     f"      master_thread_id: {master_thread_id}\n"
339                     f"      latency_thread_id: {latency_thread_id}\n"
340                     f"      dual_if:\n"
341                     f"          - socket: {socket}\n"
342                     f"            threads: {threads}\n"
343                     f"EOF'",
344                     sudo=True, message=u"TRex config generation!"
345                 )
346             else:
347                 raise ValueError(u"Unknown Test Type!")
348
349             TrafficGenerator.startup_trex(
350                 self._node, osi_layer, subtype=subtype
351             )
352
353     @staticmethod
354     def startup_trex(tg_node, osi_layer, subtype=None):
355         """Startup sequence for the TRex traffic generator.
356
357         :param tg_node: Traffic generator node.
358         :param osi_layer: 'L2', 'L3' or 'L7' - OSI Layer testing type.
359         :param subtype: Traffic generator sub-type.
360         :type tg_node: dict
361         :type osi_layer: str
362         :type subtype: NodeSubTypeTG
363         :raises RuntimeError: If node subtype is not a TREX or startup failed.
364         """
365         if not subtype:
366             subtype = check_subtype(tg_node)
367         if subtype == NodeSubTypeTG.TREX:
368             for _ in range(0, 3):
369                 # Kill TRex only if it is already running.
370                 cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
371                 exec_cmd_no_error(
372                     tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
373                 )
374
375                 # Configure TRex.
376                 ports = ''
377                 for port in tg_node[u"interfaces"].values():
378                     ports += f" {port.get(u'pci_address')}"
379
380                 cmd = f"sh -c \"cd {Constants.TREX_INSTALL_DIR}/scripts/ && " \
381                     f"./dpdk_nic_bind.py -u {ports} || true\""
382                 exec_cmd_no_error(
383                     tg_node, cmd, sudo=True,
384                     message=u"Unbind PCI ports from driver failed!"
385                 )
386
387                 # Start TRex.
388                 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
389                 trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
390                 trex_cmd.add(u"-i")
391                 trex_cmd.add(u"--prefix $(hostname)")
392                 trex_cmd.add(u"--hdrh")
393                 trex_cmd.add(u"--no-scapy-server")
394                 trex_cmd.add_if(u"--astf", osi_layer == u"L7")
395                 # OptionString does not create double space if extra is empty.
396                 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
397                 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
398                 cmd = f"sh -c \"{inner_command}\" > /dev/null"
399                 try:
400                     exec_cmd_no_error(tg_node, cmd, sudo=True)
401                 except RuntimeError:
402                     cmd = u"sh -c \"cat /tmp/trex.log\""
403                     exec_cmd_no_error(
404                         tg_node, cmd, sudo=True,
405                         message=u"Get TRex logs failed!"
406                     )
407                     raise RuntimeError(u"Start TRex failed!")
408
409                 # Test if TRex starts successfuly.
410                 command_line = OptionString().add(u"python3")
411                 dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
412                 command_line.add(f"'{dirname}/trex_server_info.py'")
413                 try:
414                     exec_cmd_no_error(
415                         tg_node, command_line, sudo=True,
416                         message=u"Test TRex failed!", retries=20
417                     )
418                 except RuntimeError:
419                     continue
420                 return
421             # After max retries TRex is still not responding to API critical
422             # error occurred.
423             exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
424             raise RuntimeError(u"Start TRex failed after multiple retries!")
425
426     @staticmethod
427     def is_trex_running(node):
428         """Check if TRex is running using pidof.
429
430         :param node: Traffic generator node.
431         :type node: dict
432         :returns: True if TRex is running otherwise False.
433         :rtype: bool
434         :raises RuntimeError: If node type is not a TG.
435         """
436         ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
437         return bool(int(ret) == 0)
438
439     @staticmethod
440     def teardown_traffic_generator(node):
441         """TG teardown.
442
443         :param node: Traffic generator node.
444         :type node: dict
445         :returns: nothing
446         :raises RuntimeError: If node type is not a TG,
447             or if TRex teardown fails.
448         """
449         subtype = check_subtype(node)
450         if subtype == NodeSubTypeTG.TREX:
451             exec_cmd_no_error(
452                 node,
453                 u"sh -c "
454                 u"\"if pgrep t-rex; then sudo pkill t-rex && sleep 3; fi\"",
455                 sudo=False,
456                 message=u"pkill t-rex failed"
457             )
458
459     def _parse_traffic_results(self, stdout):
460         """Parse stdout of scripts into fields of self.
461
462         Block of code to reuse, by sync start, or stop after async.
463         TODO: Is the output TG subtype dependent?
464
465         :param stdout: Text containing the standard output.
466         :type stdout: str
467         """
468         # last line from console output
469         line = stdout.splitlines()[-1]
470         self._result = line
471         logger.info(f"TrafficGen result: {self._result}")
472         self._received = self._result.split(u", ")[1].split(u"=", 1)[1]
473         self._sent = self._result.split(u", ")[2].split(u"=", 1)[1]
474         self._loss = self._result.split(u", ")[3].split(u"=", 1)[1]
475         self._approximated_duration = \
476             self._result.split(u", ")[5].split(u"=", 1)[1]
477         self._approximated_rate = self._result.split(u", ")[6].split(u"=", 1)[1]
478         self._latency = list()
479         self._latency.append(self._result.split(u", ")[7].split(u"=", 1)[1])
480         self._latency.append(self._result.split(u", ")[8].split(u"=", 1)[1])
481
482     def trex_stl_stop_remote_exec(self, node):
483         """Execute script on remote node over ssh to stop running traffic.
484
485         Internal state is updated with measurement results.
486
487         :param node: TRex generator node.
488         :type node: dict
489         :raises RuntimeError: If stop traffic script fails.
490         """
491         # No need to check subtype, we know it is TREX.
492         command_line = OptionString().add(u"python3")
493         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
494         command_line.add(f"'{dirname}/trex_stateless_stop.py'")
495         command_line.change_prefix(u"--")
496         for index, value in enumerate(self._xstats):
497             if value is not None:
498                 value = value.replace(u"'", u"\"")
499                 command_line.add_equals(f"xstat{index}", f"'{value}'")
500         stdout, _ = exec_cmd_no_error(
501             node, command_line, message=u"TRex stateless runtime error"
502         )
503         self._parse_traffic_results(stdout)
504
505     def trex_stl_start_remote_exec(
506             self, duration, rate, frame_size, traffic_profile, async_call=False,
507             latency=True, warmup_time=5.0, traffic_directions=2, tx_port=0,
508             rx_port=1):
509         """Execute script on remote node over ssh to start traffic.
510
511         In sync mode, measurement results are stored internally.
512         In async mode, initial data including xstats are stored internally.
513
514         :param duration: Time expresed in seconds for how long to send traffic.
515         :param rate: Traffic rate expressed with units (pps, %)
516         :param frame_size: L2 frame size to send (without padding and IPG).
517         :param traffic_profile: Module name as a traffic profile identifier.
518             See GPL/traffic_profiles/trex for implemented modules.
519         :param async_call: If enabled then don't wait for all incomming trafic.
520         :param latency: With latency measurement.
521         :param warmup_time: Warmup time period.
522         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
523             Default: 2
524         :param tx_port: Traffic generator transmit port for first flow.
525             Default: 0
526         :param rx_port: Traffic generator receive port for first flow.
527             Default: 1
528         :type duration: float
529         :type rate: str
530         :type frame_size: str
531         :type traffic_profile: str
532         :type async_call: bool
533         :type latency: bool
534         :type warmup_time: float
535         :type traffic_directions: int
536         :type tx_port: int
537         :type rx_port: int
538         :raises RuntimeError: In case of TG driver issue.
539         """
540         # No need to check subtype, we know it is TREX.
541         reorder = self._ifaces_reordered  # Just to make the next line fit.
542         p_0, p_1 = (rx_port, tx_port) if reorder else (tx_port, rx_port)
543         if not isinstance(duration, (float, int)):
544             duration = float(duration)
545         if not isinstance(warmup_time, (float, int)):
546             warmup_time = float(warmup_time)
547
548         command_line = OptionString().add(u"python3")
549         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
550         command_line.add(f"'{dirname}/trex_stateless_profile.py'")
551         command_line.change_prefix(u"--")
552         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
553         quoted_path = f"'{dirname}/{traffic_profile}.py'"
554         command_line.add_with_value(u"profile", quoted_path)
555         command_line.add_with_value(u"duration", f"{duration!r}")
556         command_line.add_with_value(u"frame_size", frame_size)
557         command_line.add_with_value(u"rate", f"{rate!r}")
558         command_line.add_with_value(u"warmup_time", f"{warmup_time!r}")
559         command_line.add_with_value(u"port_0", p_0)
560         command_line.add_with_value(u"port_1", p_1)
561         command_line.add_with_value(u"traffic_directions", traffic_directions)
562         command_line.add_if(u"async_start", async_call)
563         command_line.add_if(u"latency", latency)
564         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
565
566         stdout, _ = exec_cmd_no_error(
567             self._node, command_line, timeout=float(duration) + 60,
568             message=u"TRex stateless runtime error"
569         )
570
571         self.traffic_directions = traffic_directions
572         if async_call:
573             # no result
574             self._start_time = time.time()
575             self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
576             self._received = None
577             self._sent = None
578             self._loss = None
579             self._latency = None
580             xstats = [None, None]
581             index = 0
582             for line in stdout.splitlines():
583                 if f"Xstats snapshot {index}: " in line:
584                     xstats[index] = line[19:]
585                     index += 1
586                 if index == 2:
587                     break
588             self._xstats = tuple(xstats)
589         else:
590             self._parse_traffic_results(stdout)
591             self._start_time = None
592             self._rate = None
593
594     def stop_traffic_on_tg(self):
595         """Stop all traffic on TG.
596
597         :returns: Structure containing the result of the measurement.
598         :rtype: ReceiveRateMeasurement
599         :raises RuntimeError: If TG is not set.
600         """
601         subtype = check_subtype(self._node)
602         if subtype == NodeSubTypeTG.TREX:
603             self.trex_stl_stop_remote_exec(self._node)
604         return self.get_measurement_result()
605
606     def send_traffic_on_tg(
607             self, duration, rate, frame_size, traffic_profile, warmup_time=5,
608             async_call=False, latency=True, traffic_directions=2, tx_port=0,
609             rx_port=1):
610         """Send traffic from all configured interfaces on TG.
611
612         In async mode, xstats is stored internally,
613         to enable getting correct result when stopping the traffic.
614         In both modes, stdout is returned,
615         but _parse_traffic_results only works in sync output.
616
617         Note that bidirectional traffic also contains flows
618         transmitted from rx_port and received in tx_port.
619         But some tests use asymmetric traffic, so those arguments are relevant.
620
621         Also note that traffic generator uses DPDK driver which might
622         reorder port numbers based on wiring and PCI numbering.
623         This method handles that, so argument values are invariant,
624         but you can see swapped valued in debug logs.
625
626         TODO: Is it better to have less descriptive argument names
627         just to make them less probable to be viewed as misleading or confusing?
628         See https://gerrit.fd.io/r/#/c/17625/11/resources/libraries/python\
629         /TrafficGenerator.py@406
630
631         :param duration: Duration of test traffic generation in seconds.
632         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
633         :param frame_size: Frame size (L2) in Bytes.
634         :param traffic_profile: Module name as a traffic profile identifier.
635             See GPL/traffic_profiles/trex for implemented modules.
636         :param warmup_time: Warmup phase in seconds.
637         :param async_call: Async mode.
638         :param latency: With latency measurement.
639         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
640             Default: 2
641         :param tx_port: Traffic generator transmit port for first flow.
642             Default: 0
643         :param rx_port: Traffic generator receive port for first flow.
644             Default: 1
645         :type duration: str
646         :type rate: str
647         :type frame_size: str
648         :type traffic_profile: str
649         :type warmup_time: float
650         :type async_call: bool
651         :type latency: bool
652         :type traffic_directions: int
653         :type tx_port: int
654         :type rx_port: int
655         :returns: TG output.
656         :rtype: str
657         :raises RuntimeError: If TG is not set, or if node is not TG,
658             or if subtype is not specified.
659         :raises NotImplementedError: If TG is not supported.
660         """
661         subtype = check_subtype(self._node)
662         if subtype == NodeSubTypeTG.TREX:
663             self.trex_stl_start_remote_exec(
664                 duration, rate, frame_size, traffic_profile, async_call,
665                 latency, warmup_time, traffic_directions, tx_port, rx_port
666             )
667
668         return self._result
669
670     def no_traffic_loss_occurred(self):
671         """Fail if loss occurred in traffic run.
672
673         :returns: nothing
674         :raises Exception: If loss occured.
675         """
676         if self._loss is None:
677             raise RuntimeError(u"The traffic generation has not been issued")
678         if self._loss != u"0":
679             raise RuntimeError(f"Traffic loss occurred: {self._loss}")
680
681     def fail_if_no_traffic_forwarded(self):
682         """Fail if no traffic forwarded.
683
684         :returns: nothing
685         :raises Exception: If no traffic forwarded.
686         """
687         if self._received is None:
688             raise RuntimeError(u"The traffic generation has not been issued")
689         if self._received == u"0":
690             raise RuntimeError(u"No traffic forwarded")
691
692     def partial_traffic_loss_accepted(
693             self, loss_acceptance, loss_acceptance_type):
694         """Fail if loss is higher then accepted in traffic run.
695
696         :param loss_acceptance: Permitted drop ratio or frames count.
697         :param loss_acceptance_type: Type of permitted loss.
698         :type loss_acceptance: float
699         :type loss_acceptance_type: LossAcceptanceType
700         :returns: nothing
701         :raises Exception: If loss is above acceptance criteria.
702         """
703         if self._loss is None:
704             raise Exception(u"The traffic generation has not been issued")
705
706         if loss_acceptance_type == u"percentage":
707             loss = (float(self._loss) / float(self._sent)) * 100
708         elif loss_acceptance_type == u"frames":
709             loss = float(self._loss)
710         else:
711             raise Exception(u"Loss acceptance type not supported")
712
713         if loss > float(loss_acceptance):
714             raise Exception(
715                 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
716             )
717
718     def set_rate_provider_defaults(
719             self, frame_size, traffic_profile, warmup_time=0.0,
720             traffic_directions=2):
721         """Store values accessed by measure().
722
723         :param frame_size: Frame size identifier or value [B].
724         :param traffic_profile: Module name as a traffic profile identifier.
725             See GPL/traffic_profiles/trex for implemented modules.
726         :param warmup_time: Traffic duration before measurement starts [s].
727         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
728             Default: 2
729         :type frame_size: str or int
730         :type traffic_profile: str
731         :type warmup_time: float
732         :type traffic_directions: int
733         """
734         self.frame_size = frame_size
735         self.traffic_profile = str(traffic_profile)
736         self.warmup_time = float(warmup_time)
737         self.traffic_directions = traffic_directions
738
739     def get_measurement_result(self, duration=None, transmit_rate=None):
740         """Return the result of last measurement as ReceiveRateMeasurement.
741
742         Separate function, as measurements can end either by time
743         or by explicit call, this is the common block at the end.
744
745         TODO: Fail on running or already reported measurement.
746
747         :param duration: Measurement duration [s] if known beforehand.
748             For explicitly stopped measurement it is estimated.
749         :param transmit_rate: Target aggregate transmit rate [pps].
750             If not given, computed assuming it was bidirectional.
751         :type duration: float or NoneType
752         :type transmit_rate: float or NoneType
753         :returns: Structure containing the result of the measurement.
754         :rtype: ReceiveRateMeasurement
755         """
756         if duration is None:
757             duration = time.time() - self._start_time
758             self._start_time = None
759         if transmit_rate is None:
760             transmit_rate = self._rate * self.traffic_directions
761         transmit_count = int(self.get_sent())
762         loss_count = int(self.get_loss())
763         measurement = ReceiveRateMeasurement(
764             duration, transmit_rate, transmit_count, loss_count
765         )
766         measurement.latency = self.get_latency_int()
767         measurement.approximated_rate = self.get_approximated_rate()
768         return measurement
769
770     def measure(self, duration, transmit_rate):
771         """Run trial measurement, parse and return aggregate results.
772
773         Aggregate means sum over traffic directions.
774
775         :param duration: Trial duration [s].
776         :param transmit_rate: Target aggregate transmit rate [pps].
777         :type duration: float
778         :type transmit_rate: float
779         :returns: Structure containing the result of the measurement.
780         :rtype: ReceiveRateMeasurement
781         :raises RuntimeError: If TG is not set, or if node is not TG,
782             or if subtype is not specified.
783         :raises NotImplementedError: If TG is not supported.
784         """
785         duration = float(duration)
786         transmit_rate = float(transmit_rate)
787         # TG needs target Tr per stream, but reports aggregate Tx and Dx.
788         unit_rate_int = transmit_rate / float(self.traffic_directions)
789         unit_rate_str = str(unit_rate_int) + u"pps"
790         self.send_traffic_on_tg(
791             duration, unit_rate_str, self.frame_size, self.traffic_profile,
792             warmup_time=self.warmup_time, latency=True,
793             traffic_directions=self.traffic_directions
794         )
795         return self.get_measurement_result(duration, transmit_rate)
796
797
798 class OptimizedSearch:
799     """Class to be imported as Robot Library, containing search keywords.
800
801     Aside of setting up measurer and forwarding arguments,
802     the main business is to translate min/max rate from unidir to aggregate.
803     """
804
805     @staticmethod
806     def perform_optimized_ndrpdr_search(
807             frame_size, traffic_profile, minimum_transmit_rate,
808             maximum_transmit_rate, packet_loss_ratio=0.005,
809             final_relative_width=0.005, final_trial_duration=30.0,
810             initial_trial_duration=1.0, number_of_intermediate_phases=2,
811             timeout=720.0, doublings=1, traffic_directions=2):
812         """Setup initialized TG, perform optimized search, return intervals.
813
814         :param frame_size: Frame size identifier or value [B].
815         :param traffic_profile: Module name as a traffic profile identifier.
816             See GPL/traffic_profiles/trex for implemented modules.
817         :param minimum_transmit_rate: Minimal uni-directional
818             target transmit rate [pps].
819         :param maximum_transmit_rate: Maximal uni-directional
820             target transmit rate [pps].
821         :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
822         :param final_relative_width: Final lower bound transmit rate
823             cannot be more distant that this multiple of upper bound [1].
824         :param final_trial_duration: Trial duration for the final phase [s].
825         :param initial_trial_duration: Trial duration for the initial phase
826             and also for the first intermediate phase [s].
827         :param number_of_intermediate_phases: Number of intermediate phases
828             to perform before the final phase [1].
829         :param timeout: The search will fail itself when not finished
830             before this overall time [s].
831         :param doublings: How many doublings to do in external search step.
832             Default 1 is suitable for fairly stable tests,
833             less stable tests might get better overal duration with 2 or more.
834         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
835             Default: 2
836         :type frame_size: str or int
837         :type traffic_profile: str
838         :type minimum_transmit_rate: float
839         :type maximum_transmit_rate: float
840         :type packet_loss_ratio: float
841         :type final_relative_width: float
842         :type final_trial_duration: float
843         :type initial_trial_duration: float
844         :type number_of_intermediate_phases: int
845         :type timeout: float
846         :type doublings: int
847         :type traffic_directions: int
848         :returns: Structure containing narrowed down NDR and PDR intervals
849             and their measurements.
850         :rtype: NdrPdrResult
851         :raises RuntimeError: If total duration is larger than timeout.
852         """
853         minimum_transmit_rate *= traffic_directions
854         maximum_transmit_rate *= traffic_directions
855         # we need instance of TrafficGenerator instantiated by Robot Framework
856         # to be able to use trex_stl-*()
857         tg_instance = BuiltIn().get_library_instance(
858             u"resources.libraries.python.TrafficGenerator"
859         )
860         tg_instance.set_rate_provider_defaults(
861             frame_size, traffic_profile, traffic_directions=traffic_directions)
862         algorithm = MultipleLossRatioSearch(
863             measurer=tg_instance, final_trial_duration=final_trial_duration,
864             final_relative_width=final_relative_width,
865             number_of_intermediate_phases=number_of_intermediate_phases,
866             initial_trial_duration=initial_trial_duration, timeout=timeout,
867             doublings=doublings
868         )
869         result = algorithm.narrow_down_ndr_and_pdr(
870             minimum_transmit_rate, maximum_transmit_rate, packet_loss_ratio
871         )
872         return result
873
874     @staticmethod
875     def perform_soak_search(
876             frame_size, traffic_profile, minimum_transmit_rate,
877             maximum_transmit_rate, plr_target=1e-7, tdpt=0.1,
878             initial_count=50, timeout=1800.0, trace_enabled=False,
879             traffic_directions=2):
880         """Setup initialized TG, perform soak search, return avg and stdev.
881
882         :param frame_size: Frame size identifier or value [B].
883         :param traffic_profile: Module name as a traffic profile identifier.
884             See GPL/traffic_profiles/trex for implemented modules.
885         :param minimum_transmit_rate: Minimal uni-directional
886             target transmit rate [pps].
887         :param maximum_transmit_rate: Maximal uni-directional
888             target transmit rate [pps].
889         :param plr_target: Fraction of packets lost to achieve [1].
890         :param tdpt: Trial duration per trial.
891             The algorithm linearly increases trial duration with trial number,
892             this is the increment between succesive trials, in seconds.
893         :param initial_count: Offset to apply before the first trial.
894             For example initial_count=50 makes first trial to be 51*tdpt long.
895             This is needed because initial "search" phase of integrator
896             takes significant time even without any trial results.
897         :param timeout: The search will stop after this overall time [s].
898         :param trace_enabled: True if trace enabled else False.
899         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
900             Default: 2
901         :type frame_size: str or int
902         :type traffic_profile: str
903         :type minimum_transmit_rate: float
904         :type maximum_transmit_rate: float
905         :type plr_target: float
906         :type initial_count: int
907         :type timeout: float
908         :type trace_enabled: bool
909         :type traffic_directions: int
910         :returns: Average and stdev of estimated aggregate rate giving PLR.
911         :rtype: 2-tuple of float
912         """
913         minimum_transmit_rate *= traffic_directions
914         maximum_transmit_rate *= traffic_directions
915         tg_instance = BuiltIn().get_library_instance(
916             u"resources.libraries.python.TrafficGenerator"
917         )
918         tg_instance.set_rate_provider_defaults(
919             frame_size, traffic_profile, traffic_directions=traffic_directions)
920         algorithm = PLRsearch(
921             measurer=tg_instance, trial_duration_per_trial=tdpt,
922             packet_loss_ratio_target=plr_target,
923             trial_number_offset=initial_count, timeout=timeout,
924             trace_enabled=trace_enabled
925         )
926         result = algorithm.search(minimum_transmit_rate, maximum_transmit_rate)
927         return result