FIX: Various issues
[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(u"--no-scapy-server")
355             trex_cmd.add_if(u"--astf", osi_layer == u"L7")
356             # OptionString does not create double space if extra is empty.
357             trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
358             inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
359             cmd = f"sh -c \"{inner_command}\" > /dev/null"
360             try:
361                 exec_cmd_no_error(self._node, cmd, sudo=True)
362             except RuntimeError:
363                 cmd = u"sh -c \"cat /tmp/trex.log\""
364                 exec_cmd_no_error(
365                     self._node, cmd, sudo=True, message=u"Get TRex logs failed!"
366                 )
367                 raise RuntimeError(u"Start TRex failed!")
368
369             # Test if TRex starts successfuly.
370             cmd = f"sh -c \"{Constants.REMOTE_FW_DIR}/resources/tools/trex/" \
371                 f"trex_server_info.py\""
372             try:
373                 exec_cmd_no_error(
374                     self._node, cmd, sudo=True, message=u"Test TRex failed!",
375                     retries=20
376                 )
377             except RuntimeError:
378                 continue
379             return
380         # After max retries TRex is still not responding to API critical error
381         # occurred.
382         exec_cmd(self._node, u"cat /tmp/trex.log", sudo=True)
383         raise RuntimeError(u"Start TRex failed after multiple retries!")
384
385     @staticmethod
386     def is_trex_running(node):
387         """Check if TRex is running using pidof.
388
389         :param node: Traffic generator node.
390         :type node: dict
391         :returns: True if TRex is running otherwise False.
392         :rtype: bool
393         :raises RuntimeError: If node type is not a TG.
394         """
395         # No need to check subtype, we know it is TREX.
396
397         ret, _, _ = exec_cmd(node, u"pidof t-rex", sudo=True)
398         return bool(int(ret) == 0)
399
400     @staticmethod
401     def teardown_traffic_generator(node):
402         """TG teardown.
403
404         :param node: Traffic generator node.
405         :type node: dict
406         :returns: nothing
407         :raises RuntimeError: If node type is not a TG,
408             or if TRex teardown fails.
409         """
410         subtype = check_subtype(node)
411         if subtype == NodeSubTypeTG.TREX:
412             exec_cmd_no_error(
413                 node, u"sh -c \"sudo pkill t-rex && sleep 3\"",
414                 sudo=False, message=u"pkill t-rex failed"
415             )
416
417     def _parse_traffic_results(self, stdout):
418         """Parse stdout of scripts into fields of self.
419
420         Block of code to reuse, by sync start, or stop after async.
421         TODO: Is the output TG subtype dependent?
422
423         :param stdout: Text containing the standard output.
424         :type stdout: str
425         """
426         # last line from console output
427         line = stdout.splitlines()[-1]
428         self._result = line
429         logger.info(f"TrafficGen result: {self._result}")
430         self._received = self._result.split(u", ")[1].split(u"=", 1)[1]
431         self._sent = self._result.split(u", ")[2].split(u"=", 1)[1]
432         self._loss = self._result.split(u", ")[3].split(u"=", 1)[1]
433         self._latency = list()
434         self._latency.append(self._result.split(u", ")[4].split(u"=", 1)[1])
435         self._latency.append(self._result.split(u", ")[5].split(u"=", 1)[1])
436
437     def trex_stl_stop_remote_exec(self, node):
438         """Execute script on remote node over ssh to stop running traffic.
439
440         Internal state is updated with measurement results.
441
442         :param node: TRex generator node.
443         :type node: dict
444         :raises RuntimeError: If stop traffic script fails.
445         """
446         # No need to check subtype, we know it is TREX.
447         x_args = u""
448         for index, value in enumerate(self._xstats):
449             if value is not None:
450                 # Nested quoting is fun.
451                 value = value.replace(u"'", u"\"")
452                 x_args += f" --xstat{index}='\"'\"'{value}'\"'\"'"
453         stdout, _ = exec_cmd_no_error(
454             node, f"sh -c '{Constants.REMOTE_FW_DIR}/resources/tools/trex/"
455             f"trex_stateless_stop.py{x_args}'",
456             message=u"TRex stateless runtime error"
457         )
458         self._parse_traffic_results(stdout)
459
460     def trex_stl_start_remote_exec(
461             self, duration, rate, frame_size, traffic_profile, async_call=False,
462             latency=True, warmup_time=5.0, traffic_directions=2, tx_port=0,
463             rx_port=1):
464         """Execute script on remote node over ssh to start traffic.
465
466         In sync mode, measurement results are stored internally.
467         In async mode, initial data including xstats are stored internally.
468
469         :param duration: Time expresed in seconds for how long to send traffic.
470         :param rate: Traffic rate expressed with units (pps, %)
471         :param frame_size: L2 frame size to send (without padding and IPG).
472         :param traffic_profile: Module name as a traffic profile identifier.
473             See resources/traffic_profiles/trex for implemented modules.
474         :param async_call: If enabled then don't wait for all incomming trafic.
475         :param latency: With latency measurement.
476         :param warmup_time: Warmup time period.
477         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
478             Default: 2
479         :param tx_port: Traffic generator transmit port for first flow.
480             Default: 0
481         :param rx_port: Traffic generator receive port for first flow.
482             Default: 1
483         :type duration: float
484         :type rate: str
485         :type frame_size: str
486         :type traffic_profile: str
487         :type async_call: bool
488         :type latency: bool
489         :type warmup_time: float
490         :type traffic_directions: int
491         :type tx_port: int
492         :type rx_port: int
493         :raises RuntimeError: In case of TG driver issue.
494         """
495         # No need to check subtype, we know it is TREX.
496         reorder = self._ifaces_reordered  # Just to make the next line fit.
497         p_0, p_1 = (rx_port, tx_port) if reorder else (tx_port, rx_port)
498
499         if not isinstance(duration, (float, int)):
500             duration = float(duration)
501         if not isinstance(warmup_time, (float, int)):
502             warmup_time = float(warmup_time)
503         command = f"sh -c \"" \
504             f"{Constants.REMOTE_FW_DIR}/resources/tools/trex/" \
505             f"trex_stateless_profile.py" \
506             f" --profile {Constants.REMOTE_FW_DIR}/resources/" \
507             f"traffic_profiles/trex/{traffic_profile}.py" \
508             f" --duration {duration!r} --frame_size {frame_size} " \
509             f"--rate {rate!r} --warmup_time {warmup_time!r} " \
510             f"--port_0 {p_0} --port_1 {p_1}" \
511             f" --traffic_directions {traffic_directions}"
512         if async_call:
513             command += u" --async_start"
514         if latency:
515             command += u" --latency"
516         command += u"\""
517
518         stdout, _ = exec_cmd_no_error(
519             self._node, command, timeout=float(duration) + 60,
520             message=u"TRex stateless runtime error"
521         )
522
523         self.traffic_directions = traffic_directions
524         if async_call:
525             # no result
526             self._start_time = time.time()
527             self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
528             self._received = None
529             self._sent = None
530             self._loss = None
531             self._latency = None
532             xstats = [None, None]
533             index = 0
534             for line in stdout.splitlines():
535                 if f"Xstats snapshot {index}: " in line:
536                     xstats[index] = line[19:]
537                     index += 1
538                 if index == 2:
539                     break
540             self._xstats = tuple(xstats)
541         else:
542             self._parse_traffic_results(stdout)
543             self._start_time = None
544             self._rate = None
545
546     def stop_traffic_on_tg(self):
547         """Stop all traffic on TG.
548
549         :returns: Structure containing the result of the measurement.
550         :rtype: ReceiveRateMeasurement
551         :raises RuntimeError: If TG is not set.
552         """
553         subtype = check_subtype(self._node)
554         if subtype == NodeSubTypeTG.TREX:
555             self.trex_stl_stop_remote_exec(self._node)
556         return self.get_measurement_result()
557
558     def send_traffic_on_tg(
559             self, duration, rate, frame_size, traffic_profile, warmup_time=5,
560             async_call=False, latency=True, traffic_directions=2, tx_port=0,
561             rx_port=1):
562         """Send traffic from all configured interfaces on TG.
563
564         In async mode, xstats is stored internally,
565         to enable getting correct result when stopping the traffic.
566         In both modes, stdout is returned,
567         but _parse_traffic_results only works in sync output.
568
569         Note that bidirectional traffic also contains flows
570         transmitted from rx_port and received in tx_port.
571         But some tests use asymmetric traffic, so those arguments are relevant.
572
573         Also note that traffic generator uses DPDK driver which might
574         reorder port numbers based on wiring and PCI numbering.
575         This method handles that, so argument values are invariant,
576         but you can see swapped valued in debug logs.
577
578         TODO: Is it better to have less descriptive argument names
579         just to make them less probable to be viewed as misleading or confusing?
580         See https://gerrit.fd.io/r/#/c/17625/11/resources/libraries/python\
581         /TrafficGenerator.py@406
582
583         :param duration: Duration of test traffic generation in seconds.
584         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
585         :param frame_size: Frame size (L2) in Bytes.
586         :param traffic_profile: Module name as a traffic profile identifier.
587             See resources/traffic_profiles/trex for implemented modules.
588         :param warmup_time: Warmup phase in seconds.
589         :param async_call: Async mode.
590         :param latency: With latency measurement.
591         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
592             Default: 2
593         :param tx_port: Traffic generator transmit port for first flow.
594             Default: 0
595         :param rx_port: Traffic generator receive port for first flow.
596             Default: 1
597         :type duration: str
598         :type rate: str
599         :type frame_size: str
600         :type traffic_profile: str
601         :type warmup_time: float
602         :type async_call: bool
603         :type latency: bool
604         :type traffic_directions: int
605         :type tx_port: int
606         :type rx_port: int
607         :returns: TG output.
608         :rtype: str
609         :raises RuntimeError: If TG is not set, or if node is not TG,
610             or if subtype is not specified.
611         :raises NotImplementedError: If TG is not supported.
612         """
613         subtype = check_subtype(self._node)
614         if subtype == NodeSubTypeTG.TREX:
615             self.trex_stl_start_remote_exec(
616                 duration, rate, frame_size, traffic_profile, async_call,
617                 latency, warmup_time, traffic_directions, tx_port, rx_port
618             )
619
620         return self._result
621
622     def no_traffic_loss_occurred(self):
623         """Fail if loss occurred in traffic run.
624
625         :returns: nothing
626         :raises Exception: If loss occured.
627         """
628         if self._loss is None:
629             raise RuntimeError(u"The traffic generation has not been issued")
630         if self._loss != u"0":
631             raise RuntimeError(f"Traffic loss occurred: {self._loss}")
632
633     def fail_if_no_traffic_forwarded(self):
634         """Fail if no traffic forwarded.
635
636         :returns: nothing
637         :raises Exception: If no traffic forwarded.
638         """
639         if self._received is None:
640             raise RuntimeError(u"The traffic generation has not been issued")
641         if self._received == u"0":
642             raise RuntimeError(u"No traffic forwarded")
643
644     def partial_traffic_loss_accepted(
645             self, loss_acceptance, loss_acceptance_type):
646         """Fail if loss is higher then accepted in traffic run.
647
648         :param loss_acceptance: Permitted drop ratio or frames count.
649         :param loss_acceptance_type: Type of permitted loss.
650         :type loss_acceptance: float
651         :type loss_acceptance_type: LossAcceptanceType
652         :returns: nothing
653         :raises Exception: If loss is above acceptance criteria.
654         """
655         if self._loss is None:
656             raise Exception(u"The traffic generation has not been issued")
657
658         if loss_acceptance_type == u"percentage":
659             loss = (float(self._loss) / float(self._sent)) * 100
660         elif loss_acceptance_type == u"frames":
661             loss = float(self._loss)
662         else:
663             raise Exception(u"Loss acceptance type not supported")
664
665         if loss > float(loss_acceptance):
666             raise Exception(
667                 f"Traffic loss {loss} above loss acceptance: {loss_acceptance}"
668             )
669
670     def set_rate_provider_defaults(
671             self, frame_size, traffic_profile, warmup_time=0.0,
672             traffic_directions=2):
673         """Store values accessed by measure().
674
675         :param frame_size: Frame size identifier or value [B].
676         :param traffic_profile: Module name as a traffic profile identifier.
677             See resources/traffic_profiles/trex for implemented modules.
678         :param warmup_time: Traffic duration before measurement starts [s].
679         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
680             Default: 2
681         :type frame_size: str or int
682         :type traffic_profile: str
683         :type warmup_time: float
684         :type traffic_directions: int
685         """
686         self.frame_size = frame_size
687         self.traffic_profile = str(traffic_profile)
688         self.warmup_time = float(warmup_time)
689         self.traffic_directions = traffic_directions
690
691     def get_measurement_result(self, duration=None, transmit_rate=None):
692         """Return the result of last measurement as ReceiveRateMeasurement.
693
694         Separate function, as measurements can end either by time
695         or by explicit call, this is the common block at the end.
696
697         TODO: Fail on running or already reported measurement.
698
699         :param duration: Measurement duration [s] if known beforehand.
700             For explicitly stopped measurement it is estimated.
701         :param transmit_rate: Target aggregate transmit rate [pps].
702             If not given, computed assuming it was bidirectional.
703         :type duration: float or NoneType
704         :type transmit_rate: float or NoneType
705         :returns: Structure containing the result of the measurement.
706         :rtype: ReceiveRateMeasurement
707         """
708         if duration is None:
709             duration = time.time() - self._start_time
710             self._start_time = None
711         if transmit_rate is None:
712             transmit_rate = self._rate * self.traffic_directions
713         transmit_count = int(self.get_sent())
714         loss_count = int(self.get_loss())
715         measurement = ReceiveRateMeasurement(
716             duration, transmit_rate, transmit_count, loss_count
717         )
718         measurement.latency = self.get_latency_int()
719         return measurement
720
721     def measure(self, duration, transmit_rate):
722         """Run trial measurement, parse and return aggregate results.
723
724         Aggregate means sum over traffic directions.
725
726         :param duration: Trial duration [s].
727         :param transmit_rate: Target aggregate transmit rate [pps].
728         :type duration: float
729         :type transmit_rate: float
730         :returns: Structure containing the result of the measurement.
731         :rtype: ReceiveRateMeasurement
732         :raises RuntimeError: If TG is not set, or if node is not TG,
733             or if subtype is not specified.
734         :raises NotImplementedError: If TG is not supported.
735         """
736         duration = float(duration)
737         transmit_rate = float(transmit_rate)
738         # TG needs target Tr per stream, but reports aggregate Tx and Dx.
739         unit_rate_int = transmit_rate / float(self.traffic_directions)
740         unit_rate_str = str(unit_rate_int) + u"pps"
741         self.send_traffic_on_tg(
742             duration, unit_rate_str, self.frame_size, self.traffic_profile,
743             warmup_time=self.warmup_time, latency=True,
744             traffic_directions=self.traffic_directions
745         )
746         return self.get_measurement_result(duration, transmit_rate)
747
748
749 class OptimizedSearch:
750     """Class to be imported as Robot Library, containing search keywords.
751
752     Aside of setting up measurer and forwarding arguments,
753     the main business is to translate min/max rate from unidir to aggregate.
754     """
755
756     @staticmethod
757     def perform_optimized_ndrpdr_search(
758             frame_size, traffic_profile, minimum_transmit_rate,
759             maximum_transmit_rate, packet_loss_ratio=0.005,
760             final_relative_width=0.005, final_trial_duration=30.0,
761             initial_trial_duration=1.0, number_of_intermediate_phases=2,
762             timeout=720.0, doublings=1, traffic_directions=2):
763         """Setup initialized TG, perform optimized search, return intervals.
764
765         :param frame_size: Frame size identifier or value [B].
766         :param traffic_profile: Module name as a traffic profile identifier.
767             See resources/traffic_profiles/trex for implemented modules.
768         :param minimum_transmit_rate: Minimal uni-directional
769             target transmit rate [pps].
770         :param maximum_transmit_rate: Maximal uni-directional
771             target transmit rate [pps].
772         :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
773         :param final_relative_width: Final lower bound transmit rate
774             cannot be more distant that this multiple of upper bound [1].
775         :param final_trial_duration: Trial duration for the final phase [s].
776         :param initial_trial_duration: Trial duration for the initial phase
777             and also for the first intermediate phase [s].
778         :param number_of_intermediate_phases: Number of intermediate phases
779             to perform before the final phase [1].
780         :param timeout: The search will fail itself when not finished
781             before this overall time [s].
782         :param doublings: How many doublings to do in external search step.
783             Default 1 is suitable for fairly stable tests,
784             less stable tests might get better overal duration with 2 or more.
785         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
786             Default: 2
787         :type frame_size: str or int
788         :type traffic_profile: str
789         :type minimum_transmit_rate: float
790         :type maximum_transmit_rate: float
791         :type packet_loss_ratio: float
792         :type final_relative_width: float
793         :type final_trial_duration: float
794         :type initial_trial_duration: float
795         :type number_of_intermediate_phases: int
796         :type timeout: float
797         :type doublings: int
798         :type traffic_directions: int
799         :returns: Structure containing narrowed down NDR and PDR intervals
800             and their measurements.
801         :rtype: NdrPdrResult
802         :raises RuntimeError: If total duration is larger than timeout.
803         """
804         minimum_transmit_rate *= traffic_directions
805         maximum_transmit_rate *= traffic_directions
806         # we need instance of TrafficGenerator instantiated by Robot Framework
807         # to be able to use trex_stl-*()
808         tg_instance = BuiltIn().get_library_instance(
809             u"resources.libraries.python.TrafficGenerator"
810         )
811         tg_instance.set_rate_provider_defaults(
812             frame_size, traffic_profile, traffic_directions=traffic_directions)
813         algorithm = MultipleLossRatioSearch(
814             measurer=tg_instance, final_trial_duration=final_trial_duration,
815             final_relative_width=final_relative_width,
816             number_of_intermediate_phases=number_of_intermediate_phases,
817             initial_trial_duration=initial_trial_duration, timeout=timeout,
818             doublings=doublings
819         )
820         result = algorithm.narrow_down_ndr_and_pdr(
821             minimum_transmit_rate, maximum_transmit_rate, packet_loss_ratio
822         )
823         return result
824
825     @staticmethod
826     def perform_soak_search(
827             frame_size, traffic_profile, minimum_transmit_rate,
828             maximum_transmit_rate, plr_target=1e-7, tdpt=0.1,
829             initial_count=50, timeout=1800.0, trace_enabled=False,
830             traffic_directions=2):
831         """Setup initialized TG, perform soak search, return avg and stdev.
832
833         :param frame_size: Frame size identifier or value [B].
834         :param traffic_profile: Module name as a traffic profile identifier.
835             See resources/traffic_profiles/trex for implemented modules.
836         :param minimum_transmit_rate: Minimal uni-directional
837             target transmit rate [pps].
838         :param maximum_transmit_rate: Maximal uni-directional
839             target transmit rate [pps].
840         :param plr_target: Fraction of packets lost to achieve [1].
841         :param tdpt: Trial duration per trial.
842             The algorithm linearly increases trial duration with trial number,
843             this is the increment between succesive trials, in seconds.
844         :param initial_count: Offset to apply before the first trial.
845             For example initial_count=50 makes first trial to be 51*tdpt long.
846             This is needed because initial "search" phase of integrator
847             takes significant time even without any trial results.
848         :param timeout: The search will stop after this overall time [s].
849         :param trace_enabled: True if trace enabled else False.
850         :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
851             Default: 2
852         :type frame_size: str or int
853         :type traffic_profile: str
854         :type minimum_transmit_rate: float
855         :type maximum_transmit_rate: float
856         :type plr_target: float
857         :type initial_count: int
858         :type timeout: float
859         :type trace_enabled: bool
860         :type traffic_directions: int
861         :returns: Average and stdev of estimated aggregate rate giving PLR.
862         :rtype: 2-tuple of float
863         """
864         minimum_transmit_rate *= traffic_directions
865         maximum_transmit_rate *= traffic_directions
866         tg_instance = BuiltIn().get_library_instance(
867             u"resources.libraries.python.TrafficGenerator"
868         )
869         tg_instance.set_rate_provider_defaults(
870             frame_size, traffic_profile, traffic_directions=traffic_directions)
871         algorithm = PLRsearch(
872             measurer=tg_instance, trial_duration_per_trial=tdpt,
873             packet_loss_ratio_target=plr_target,
874             trial_number_offset=initial_count, timeout=timeout,
875             trace_enabled=trace_enabled
876         )
877         result = algorithm.search(minimum_transmit_rate, maximum_transmit_rate)
878         return result