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