feat(core): Multilink TRex Sync mode II 78/39678/16
authorpmikus <peter.mikus@protonmail.ch>
Fri, 13 Oct 2023 12:34:04 +0000 (12:34 +0000)
committerPeter Mikus <peter.mikus@protonmail.ch>
Wed, 25 Oct 2023 14:30:07 +0000 (14:30 +0000)
Signed-off-by: Peter Mikus <peter.mikus@protonmail.ch>
Change-Id: Idf4df372520d4a72c0738a89290d16710485f140

GPL/tools/trex/trex_stl_profile.py
GPL/traffic_profiles/trex/profile_trex_stateless_scale_class.py [new file with mode: 0644]
GPL/traffic_profiles/trex/trex-stl-ethip4-ip4dst10000p3.py [new file with mode: 0644]
resources/libraries/python/TrafficGenerator.py

index c318674..ac53e90 100644 (file)
@@ -74,8 +74,7 @@ def simple_burst(
         duration,
         framesize,
         rate,
-        port_0,
-        port_1,
+        ports,
         latency,
         async_start=False,
         traffic_directions=2,
@@ -103,8 +102,7 @@ def simple_burst(
     :param framesize: Frame size.
     :param duration: Duration of traffic run in seconds (-1=infinite).
     :param rate: Traffic rate [percentage, pps, bps].
-    :param port_0: Port 0 on the traffic generator.
-    :param port_1: Port 1 on the traffic generator.
+    :param ports: Port list on the traffic generator.
     :param latency: With latency stats.
     :param async_start: Start the traffic and exit.
     :param traffic_directions: Bidirectional (2) or unidirectional (1) traffic.
@@ -114,8 +112,7 @@ def simple_burst(
     :type framesize: int or str
     :type duration: float
     :type rate: str
-    :type port_0: int
-    :type port_1: int
+    :type ports: list
     :type latency: bool
     :type async_start: bool
     :type traffic_directions: int
@@ -153,38 +150,47 @@ def simple_burst(
         if "macsrc" in profile_file:
             client.set_port_attr(promiscuous=True)
         if isinstance(framesize, int):
-            last_stream_a = int((len(streams) - 2) / 2)
-            last_stream_b = (last_stream_a * 2)
-            client.add_streams(streams[0:last_stream_a], ports=[port_0])
+            mark_a = len(streams) // 4
+            mark_b = len(streams) // 2
+            for i,j in zip(streams[:mark_a], ports[::2]):
+                client.add_streams(streams=[i], ports=[j])
             if traffic_directions > 1:
-                client.add_streams(
-                    streams[last_stream_a:last_stream_b], ports=[port_1])
+                for i,j in zip(streams[mark_a:mark_b], ports[1::2]):
+                    print(i, j)
+                    client.add_streams(streams=[i], ports=[j])
         elif isinstance(framesize, str):
-            client.add_streams(streams[0:3], ports=[port_0])
+            mark = 0
+            for i in ports[::2]:
+                client.add_streams(streams=streams[mark:mark+3], ports=[i])
+                mark = mark + 3
             if traffic_directions > 1:
-                client.add_streams(streams[3:6], ports=[port_1])
+                mark = len(streams) // 2
+                for i in ports[1::2]:
+                    client.add_streams(streams=streams[mark:mark+3], ports=[i])
+                    mark = mark + 3
         if latency:
             try:
                 if isinstance(framesize, int):
-                    client.add_streams(streams[last_stream_b], ports=[port_0])
+                    mark_c = len(streams) // 2
+                    mark_d = len(streams) // 2 + len(streams) // 4
+                    for i,j in zip(streams[mark_c:mark_d], ports[::2]):
+                        client.add_streams(streams=[i], ports=[j])
                     if traffic_directions > 1:
-                        client.add_streams(
-                            streams[last_stream_b + 1], ports=[port_1])
+                        for i,j in zip(streams[mark_d:], ports[1::2]):
+                            client.add_streams(streams=[i], ports=[j])
                 elif isinstance(framesize, str):
                     latency = False
             except STLError:
                 # Disable latency if NIC does not support requested stream type
                 print("##### FAILED to add latency streams #####")
                 latency = False
-        # Even for unidir, both ports are needed to see both rx and tx.
-        ports = [port_0, port_1]
 
         # Clear the stats before injecting:
         client.clear_stats()
 
         # Choose rate and start traffic:
         client.start(
-            ports=ports[:traffic_directions],
+            ports=ports[::] if traffic_directions == 2 else ports[::2],
             mult=rate,
             duration=duration,
             force=force,
@@ -286,12 +292,8 @@ def main():
         help="Traffic rate with included units (pps)."
     )
     parser.add_argument(
-        "--port_0", required=True, type=int,
-        help="Port 0 on the traffic generator."
-    )
-    parser.add_argument(
-        "--port_1", required=True, type=int,
-        help="Port 1 on the traffic generator."
+        "--ports", required=True, type=int, nargs="+",
+        help="Port list on the traffic generator."
     )
     parser.add_argument(
         "--async_start", action="store_true", default=False,
@@ -326,8 +328,7 @@ def main():
         duration=args.duration,
         framesize=framesize,
         rate=args.rate,
-        port_0=args.port_0,
-        port_1=args.port_1,
+        ports=args.ports,
         latency=args.latency,
         async_start=args.async_start,
         traffic_directions=args.traffic_directions,
diff --git a/GPL/traffic_profiles/trex/profile_trex_stateless_scale_class.py b/GPL/traffic_profiles/trex/profile_trex_stateless_scale_class.py
new file mode 100644 (file)
index 0000000..33cf090
--- /dev/null
@@ -0,0 +1,196 @@
+# Copyright (c) 2023 Cisco and/or its affiliates.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+#
+# Licensed under the Apache License 2.0 or
+# GNU General Public License v2.0 or later;  you may not use this file
+# except in compliance with one of these Licenses. You
+# may obtain a copy of the Licenses at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#     https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
+#
+# Note: If this file is linked with Scapy, which is GPLv2+, your use of it
+# must be under GPLv2+.  If at any point in the future it is no longer linked
+# with Scapy (or other GPLv2+ licensed software), you are free to choose
+# Apache 2.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Base class for stream profiles for T-rex traffic generator.
+"""
+
+import socket
+import struct
+
+from random import choice
+from string import ascii_letters
+
+from trex.stl.api import *
+
+
+class TrafficStreamsScaleClass:
+    """Base class for stream profiles for T-rex traffic generator."""
+
+    STREAM_TABLE = {
+        "IMIX_v4": [
+            {"size": 60, "pps": 28, "isg": 0},
+            {"size": 590, "pps": 20, "isg": 0.1},
+            {"size": 1514, "pps": 4, "isg": 0.2}
+        ],
+        "IMIX_v4_1": [
+            {"size": 64, "pps": 28, "isg": 0},
+            {"size": 570, "pps": 16, "isg": 0.1},
+            {"size": 1518, "pps": 4, "isg": 0.2}
+        ]
+    }
+
+    def __init__(self):
+        # Default value of frame size, it will be overwritten by the value of
+        # "framesize" parameter of "get_streams" method.
+        self.framesize = 64
+
+        # If needed, add your own parameters.
+
+    def _gen_payload(self, length):
+        """Generate payload.
+
+        If needed, implement your own algorithm.
+
+        :param length: Length of generated payload.
+        :type length: int
+        :returns: The generated payload.
+        :rtype: str
+        """
+        payload = ""
+        for _ in range(length):
+            payload += choice(ascii_letters)
+
+        return payload
+
+    def _get_start_end_ipv6(self, start_ip, end_ip):
+        """Get start host and number of hosts from IPv6 as integer.
+
+        :param start_ip: Start IPv6.
+        :param end_ip: End IPv6.
+        :type start_ip: string
+        :type end_ip: string
+        :return: Start host, number of hosts.
+        :rtype tuple of int
+        :raises: ValueError if start_ip is greater then end_ip.
+        :raises: socket.error if the IP addresses are not valid IPv6 addresses.
+        """
+        try:
+            ip1 = socket.inet_pton(socket.AF_INET6, start_ip)
+            ip2 = socket.inet_pton(socket.AF_INET6, end_ip)
+
+            hi1, lo1 = struct.unpack("!QQ", ip1)
+            hi2, lo2 = struct.unpack("!QQ", ip2)
+
+            if ((hi1 << 64) | lo1) > ((hi2 << 64) | lo2):
+                raise ValueError("IPv6: start_ip is greater then end_ip")
+
+            return lo1, abs(int(lo1) - int(lo2))
+
+        except socket.error as err:
+            print(err)
+            raise
+
+    def define_packets(self):
+        """Define the packets to be sent from the traffic generator.
+
+        This method MUST return:
+
+            return base_pkt_a, base_pkt_b, vm1, vm2
+
+            vm1 and vm2 CAN be None.
+
+        :returns: Packets to be sent from the traffic generator.
+        :rtype: tuple
+        """
+        raise NotImplementedError
+
+    def create_streams(self):
+        """Create traffic streams.
+
+        Implement your own traffic streams.
+
+        :returns: Traffic streams.
+        :rtype: list
+        """
+        pkts, vms = self.define_packets()
+
+        # Frame size is defined as an integer, e.g. 64, 1518:
+        if isinstance(self.framesize, int):
+            pkt_streams = list()
+            lat_streams = list()
+            for i in range(len(pkts)):
+                payload_len = max(0, self.framesize - len(pkts[i]) - 4)
+
+                pkt = STLPktBuilder(
+                    pkt=pkts[i] / self._gen_payload(payload_len),
+                    vm=vms[i]
+                )
+                pkt_lat = STLPktBuilder(
+                    pkt=pkts[i] / self._gen_payload(payload_len),
+                    vm=vms[i]
+                )
+                pkt_streams.append(
+                    STLStream(
+                        packet=pkt,
+                        isg=10.0 * (i // (len(pkts) // 2)),
+                        mode=STLTXCont(pps=9000)
+                    )
+                )
+                lat_streams.append(
+                    STLStream(
+                        packet=pkt_lat,
+                        isg=10.0 * (i // (len(pkts) // 2)),
+                        flow_stats=STLFlowLatencyStats(pg_id=i),
+                        mode=STLTXCont(pps=9000)
+                    )
+                )
+
+            streams = list()
+            streams.extend(pkt_streams)
+            streams.extend(lat_streams)
+            return streams
+
+        # Frame size is defined as a string, e.g.IMIX_v4_1:
+        elif isinstance(self.framesize, str):
+            pkt_streams = list()
+            for i in range(len(pkts)):
+                for stream in self.STREAM_TABLE[self.framesize]:
+                    payload_len = max(0, stream["size"] - len(pkts[i]) - 4)
+
+                    pkt = STLPktBuilder(
+                        pkt=pkts[i] / self._gen_payload(payload_len),
+                        vm=vms[i]
+                    )
+                    pkt_streams.append(
+                        STLStream(
+                            packet=pkt,
+                            isg=stream["isg"],
+                            mode=STLTXCont(pps=stream["pps"])
+                        )
+                    )
+            return pkt_streams
+
+    def get_streams(self, **kwargs):
+        """Get traffic streams created by "create_streams" method.
+
+        If needed, add your own parameters.
+
+        :param kwargs: Key-value pairs used by "create_streams" method while
+        creating streams.
+        :returns: Traffic streams.
+        :rtype: list
+        """
+        self.framesize = kwargs["framesize"]
+        self.rate = kwargs["rate"]
+
+        return self.create_streams()
diff --git a/GPL/traffic_profiles/trex/trex-stl-ethip4-ip4dst10000p3.py b/GPL/traffic_profiles/trex/trex-stl-ethip4-ip4dst10000p3.py
new file mode 100644 (file)
index 0000000..01b6e3c
--- /dev/null
@@ -0,0 +1,135 @@
+# Copyright (c) 2023 Cisco and/or its affiliates.
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+#
+# Licensed under the Apache License 2.0 or
+# GNU General Public License v2.0 or later;  you may not use this file
+# except in compliance with one of these Licenses. You
+# may obtain a copy of the Licenses at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#     https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
+#
+# Note: If this file is linked with Scapy, which is GPLv2+, your use of it
+# must be under GPLv2+.  If at any point in the future it is no longer linked
+# with Scapy (or other GPLv2+ licensed software), you are free to choose
+# Apache 2.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Stream profile for T-rex traffic generator.
+
+Stream profile:
+ - Three parallel bi-directional streams sent as W --> E and E --> W
+   at the same time.
+ - Packet: ETH / IP /
+"""
+
+from trex.stl.api import *
+from profile_trex_stateless_scale_class import TrafficStreamsScaleClass
+
+
+class TrafficStreams(TrafficStreamsScaleClass):
+    """Stream profile."""
+
+    def __init__(self):
+        """Initialization and setting of streams' parameters."""
+
+        super(TrafficStreamsScaleClass, self).__init__()
+
+        self.pkt_data = [
+            # Direction W --> E:
+            {
+                "src_start_ip": "10.0.0.1",
+                "dst_start_ip": "20.0.0.0",
+                "dst_end_ip": "20.0.39.15"
+            },
+            # Direction W --> E:
+            {
+                "src_start_ip": "30.0.0.1",
+                "dst_start_ip": "40.0.0.0",
+                "dst_end_ip": "40.0.39.15"
+            },
+            # Direction W --> E:
+            {
+                "src_start_ip": "50.0.0.1",
+                "dst_start_ip": "60.0.0.0",
+                "dst_end_ip": "60.0.39.15"
+            },
+            # Direction E --> W:
+            {
+                "src_start_ip": "20.0.0.1",
+                "dst_start_ip": "10.0.0.0",
+                "dst_end_ip": "10.0.39.15"
+            },
+            # Direction E --> W:
+            {
+                "src_start_ip": "40.0.0.1",
+                "dst_start_ip": "30.0.0.0",
+                "dst_end_ip": "30.0.39.15"
+            },
+            # Direction E --> W:
+            {
+                "src_start_ip": "60.0.0.1",
+                "dst_start_ip": "50.0.0.0",
+                "dst_end_ip": "50.0.39.15"
+            }
+
+        ]
+        self.pkt_base = []
+        self.pkt_vm = []
+
+    def define_packets(self):
+        """Defines the packets to be sent from the traffic generator.
+
+        Packet definition: | ETH | IP |
+
+        :returns: Base packets to be sent and transformation function.
+        :rtype: tuple
+        """
+        for i in range(len(self.pkt_data)):
+            self.pkt_base.append(
+                Ether() /
+                IP(
+                    src=self.pkt_data[i]["src_start_ip"],
+                    dst=self.pkt_data[i]["dst_start_ip"],
+                    proto=61
+                )
+            )
+            self.pkt_vm.append(
+                STLScVmRaw(
+                    [
+                        STLVmFlowVar(
+                            name="dst",
+                            min_value=self.pkt_data[i]["dst_start_ip"],
+                            max_value=self.pkt_data[i]["dst_end_ip"],
+                            size=4,
+                            op="inc"
+                        ),
+                        STLVmWrFlowVar(
+                            fv_name="dst",
+                            pkt_offset="IP.dst"
+                        ),
+                        STLVmFixIpv4(
+                            offset="IP"
+                        )
+                    ]
+                )
+            )
+
+        return self.pkt_base, self.pkt_vm
+
+
+def register():
+    """Register this traffic profile to T-Rex.
+
+    Do not change this function.
+
+    :return: Traffic streams.
+    :rtype: Object
+    """
+    return TrafficStreams()
index f28c778..7c6c859 100644 (file)
@@ -143,6 +143,7 @@ class TrafficGenerator(AbstractMeasurer):
         self._mode = None
         # TG interface order mapping
         self._ifaces_reordered = False
+        self._ifaces = []
         # Result holding fields, to be removed.
         self._result = None
         self._loss = None
@@ -177,7 +178,7 @@ class TrafficGenerator(AbstractMeasurer):
         self.ramp_up_duration = None
         self.state_timeout = None
         # Transient data needed for async measurements.
-        self._xstats = ()
+        self._xstats = []
 
     @property
     def node(self):
@@ -319,23 +320,38 @@ class TrafficGenerator(AbstractMeasurer):
                         )
                     )
 
-                trex_topology.append(
-                    dict(
-                        interface=topology[f"TG_pf{link}"][0],
-                        dst_mac=tg_if1_adj_addr
-                    )
-                )
-                trex_topology.append(
-                    dict(
-                        interface=topology[f"TG_pf{link+1}"][0],
-                        dst_mac=tg_if2_adj_addr
-                    )
-                )
                 if1_pci = topology[f"TG_pf{link}_pci"][0]
                 if2_pci = topology[f"TG_pf{link+1}_pci"][0]
                 if min(if1_pci, if2_pci) != if1_pci:
-                    self._ifaces_reordered = True
-                    trex_topology.reverse()
+                    self._ifaces.append(str(link))
+                    self._ifaces.append(str(link-1))
+                    trex_topology.append(
+                        dict(
+                            interface=topology[f"TG_pf{link+1}"][0],
+                            dst_mac=tg_if2_adj_addr
+                        )
+                    )
+                    trex_topology.append(
+                        dict(
+                            interface=topology[f"TG_pf{link}"][0],
+                            dst_mac=tg_if1_adj_addr
+                        )
+                    )
+                else:
+                    self._ifaces.append(str(link-1))
+                    self._ifaces.append(str(link))
+                    trex_topology.append(
+                        dict(
+                            interface=topology[f"TG_pf{link}"][0],
+                            dst_mac=tg_if1_adj_addr
+                        )
+                    )
+                    trex_topology.append(
+                        dict(
+                            interface=topology[f"TG_pf{link+1}"][0],
+                            dst_mac=tg_if2_adj_addr
+                        )
+                    )
 
             TrexConfig.add_startup_configuration(
                 self._node, trex_topology
@@ -362,18 +378,18 @@ class TrafficGenerator(AbstractMeasurer):
         if subtype == NodeSubTypeTG.TREX:
             for _ in range(0, 3):
                 # Kill TRex only if it is already running.
-                cmd = u"sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
+                cmd = "sh -c \"pgrep t-rex && pkill t-rex && sleep 3 || true\""
                 exec_cmd_no_error(
-                    tg_node, cmd, sudo=True, message=u"Kill TRex failed!"
+                    tg_node, cmd, sudo=True, message="Kill TRex failed!"
                 )
 
                 # Prepare interfaces for TRex.
                 tg_port_drv = Constants.TREX_PORT_DRIVER
-                mlx_driver = u""
-                for port in tg_node[u"interfaces"].values():
-                    if u"Mellanox" in port.get(u"model"):
-                        mlx_driver = port.get(u"driver")
-                        pci_addr = port.get(u'pci_address')
+                mlx_driver = ""
+                for port in tg_node["interfaces"].values():
+                    if "Mellanox" in port.get("model"):
+                        mlx_driver = port.get("driver")
+                        pci_addr = port.get("pci_address")
                         cur_driver = DS.get_pci_dev_driver(tg_node, pci_addr)
                         if cur_driver == mlx_driver:
                             pass
@@ -383,7 +399,7 @@ class TrafficGenerator(AbstractMeasurer):
                             DS.pci_driver_unbind(tg_node, pci_addr)
                             DS.pci_driver_bind(tg_node, pci_addr, mlx_driver)
                     else:
-                        pci_addr = port.get(u'pci_address')
+                        pci_addr = port.get("pci_address")
                         cur_driver = DS.get_pci_dev_driver(tg_node, pci_addr)
                         if cur_driver:
                             DS.pci_driver_unbind(tg_node, pci_addr)
@@ -391,12 +407,12 @@ class TrafficGenerator(AbstractMeasurer):
 
                 # Start TRex.
                 cd_cmd = f"cd '{Constants.TREX_INSTALL_DIR}/scripts/'"
-                trex_cmd = OptionString([u"nohup", u"./t-rex-64"])
-                trex_cmd.add(u"-i")
-                trex_cmd.add(u"--prefix $(hostname)")
-                trex_cmd.add(u"--hdrh")
-                trex_cmd.add(u"--no-scapy-server")
-                trex_cmd.add_if(u"--astf", osi_layer == u"L7")
+                trex_cmd = OptionString(["nohup", "./t-rex-64"])
+                trex_cmd.add("-i")
+                trex_cmd.add("--prefix $(hostname)")
+                trex_cmd.add("--hdrh")
+                trex_cmd.add("--no-scapy-server")
+                trex_cmd.add_if("--astf", osi_layer == "L7")
                 # OptionString does not create double space if extra is empty.
                 trex_cmd.add(f"{Constants.TREX_EXTRA_CMDLINE}")
                 inner_command = f"{cd_cmd} && {trex_cmd} > /tmp/trex.log 2>&1 &"
@@ -404,33 +420,33 @@ class TrafficGenerator(AbstractMeasurer):
                 try:
                     exec_cmd_no_error(tg_node, cmd, sudo=True)
                 except RuntimeError:
-                    cmd = u"sh -c \"cat /tmp/trex.log\""
+                    cmd = "sh -c \"cat /tmp/trex.log\""
                     exec_cmd_no_error(
                         tg_node, cmd, sudo=True,
-                        message=u"Get TRex logs failed!"
+                        message="Get TRex logs failed!"
                     )
-                    raise RuntimeError(u"Start TRex failed!")
+                    raise RuntimeError("Start TRex failed!")
 
                 # Test T-Rex API responsiveness.
                 cmd = f"python3 {Constants.REMOTE_FW_DIR}/GPL/tools/trex/"
-                if osi_layer in (u"L2", u"L3"):
-                    cmd += u"trex_stl_assert.py"
-                elif osi_layer == u"L7":
-                    cmd += u"trex_astf_assert.py"
+                if osi_layer in ("L2", "L3"):
+                    cmd += "trex_stl_assert.py"
+                elif osi_layer == "L7":
+                    cmd += "trex_astf_assert.py"
                 else:
-                    raise ValueError(u"Unknown OSI layer!")
+                    raise ValueError("Unknown OSI layer!")
                 try:
                     exec_cmd_no_error(
                         tg_node, cmd, sudo=True,
-                        message=u"T-Rex API is not responding!", retries=20
+                        message="T-Rex API is not responding!", retries=20
                     )
                 except RuntimeError:
                     continue
                 return
             # After max retries TRex is still not responding to API critical
             # error occurred.
-            exec_cmd(tg_node, u"cat /tmp/trex.log", sudo=True)
-            raise RuntimeError(u"Start T-Rex failed after multiple retries!")
+            exec_cmd(tg_node, "cat /tmp/trex.log", sudo=True)
+            raise RuntimeError("Start T-Rex failed after multiple retries!")
 
     @staticmethod
     def is_trex_running(node):
@@ -441,7 +457,7 @@ class TrafficGenerator(AbstractMeasurer):
         :returns: True if T-Rex is running otherwise False.
         :rtype: bool
         """
-        ret, _, _ = exec_cmd(node, u"pgrep t-rex", sudo=True)
+        ret, _, _ = exec_cmd(node, "pgrep t-rex", sudo=True)
         return bool(int(ret) == 0)
 
     @staticmethod
@@ -718,38 +734,36 @@ class TrafficGenerator(AbstractMeasurer):
         :raises RuntimeError: In case of T-Rex driver issue.
         """
         self.check_mode(TrexMode.STL)
-        p_0, p_1 = (1, 0) if self._ifaces_reordered else (0, 1)
         if not isinstance(duration, (float, int)):
             duration = float(duration)
 
         duration, _ = self._compute_duration(duration=duration, multiplier=rate)
 
-        command_line = OptionString().add(u"python3")
+        command_line = OptionString().add("python3")
         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/tools/trex"
         command_line.add(f"'{dirname}/trex_stl_profile.py'")
-        command_line.change_prefix(u"--")
+        command_line.change_prefix("--")
         dirname = f"{Constants.REMOTE_FW_DIR}/GPL/traffic_profiles/trex"
         command_line.add_with_value(
-            u"profile", f"'{dirname}/{self.traffic_profile}.py'"
+            "profile", f"'{dirname}/{self.traffic_profile}.py'"
         )
-        command_line.add_with_value(u"duration", f"{duration!r}")
-        command_line.add_with_value(u"frame_size", self.frame_size)
-        command_line.add_with_value(u"rate", f"{rate!r}")
-        command_line.add_with_value(u"port_0", p_0)
-        command_line.add_with_value(u"port_1", p_1)
+        command_line.add_with_value("duration", f"{duration!r}")
+        command_line.add_with_value("frame_size", self.frame_size)
+        command_line.add_with_value("rate", f"{rate!r}")
+        command_line.add_with_value("ports", " ".join(self._ifaces))
         command_line.add_with_value(
-            u"traffic_directions", self.traffic_directions
+            "traffic_directions", self.traffic_directions
         )
-        command_line.add_if(u"async_start", async_call)
-        command_line.add_if(u"latency", self.use_latency)
-        command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
-        command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
+        command_line.add_if("async_start", async_call)
+        command_line.add_if("latency", self.use_latency)
+        command_line.add_if("force", Constants.TREX_SEND_FORCE)
+        command_line.add_with_value("delay", Constants.PERF_TRIAL_STL_DELAY)
 
         self._start_time = time.monotonic()
-        self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
+        self._rate = float(rate[:-3]) if "pps" in rate else float(rate)
         stdout, _ = exec_cmd_no_error(
             self._node, command_line, timeout=int(duration) + 60,
-            message=u"T-Rex STL runtime error"
+            message="T-Rex STL runtime error"
         )
 
         if async_call: