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