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