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