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