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