<div dir="ltr">Reviewed-by: Andrew Bailey <<a href="mailto:abailey@iol.unh.edu" target="_blank">abailey@iol.unh.edu</a>> </div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Wed, Nov 5, 2025 at 5:37 PM Patrick Robb <<a href="mailto:probb@iol.unh.edu">probb@iol.unh.edu</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">From: Nicholas Pratte <<a href="mailto:npratte@iol.unh.edu" target="_blank">npratte@iol.unh.edu</a>><br>
<br>
Implement the TREX traffic generator for use in the DTS framework. The<br>
provided implementation leverages TREX's stateless API automation<br>
library, via use of a Python shell. The DTS context has been modified<br>
to include a performance traffic generator in addition to a functional<br>
traffic generator.<br>
<br>
In addition, the DTS testrun state machine has been modified such that<br>
traffic generators are brought up and down as needed, and so that only<br>
one traffic generator application is running on the TG system at a time.<br>
During the testcase setup stage, the testcase type (perf or func) will<br>
be checked and the correct traffic generator brought up. For instance,<br>
if a functional TG is running from a previous test and we start a<br>
performance test, then the functional TG is stopped and the performance<br>
TG started. This is an attempt to strike a balance between the concept<br>
of having the scapy asyncsniffer always on to save on execution time,<br>
with the competing need to bring up performance traffic generators as<br>
needed. There is also an added boolean toggle for adding new shells<br>
to the current shell pool or omitting them from the shell pool in order<br>
to facilitate this new TG initialization approach.<br>
<br>
Bugzilla ID: 1697<br>
Signed-off-by: Nicholas Pratte <<a href="mailto:npratte@iol.unh.edu" target="_blank">npratte@iol.unh.edu</a>><br>
Signed-off-by: Patrick Robb <<a href="mailto:probb@iol.unh.edu" target="_blank">probb@iol.unh.edu</a>><br>
Reviewed-by: Dean Marx <<a href="mailto:dmarx@iol.unh.edu" target="_blank">dmarx@iol.unh.edu</a>><br>
Reviewed-by: Andrew Bailey <<a href="mailto:abailey@iol.unh.edu" target="_blank">abailey@iol.unh.edu</a>><br>
---<br>
 doc/guides/tools/dts.rst                      |  55 +++-<br>
 dts/api/packet.py                             |   6 +-<br>
 dts/{ => configurations}/nodes.example.yaml   |   0<br>
 .../test_run.example.yaml                     |   6 +-<br>
 .../tests_config.example.yaml                 |   0<br>
 dts/framework/config/test_run.py              |  22 +-<br>
 dts/framework/context.py                      |   5 +-<br>
 dts/framework/remote_session/blocking_app.py  |  12 +-<br>
 .../remote_session/interactive_shell.py       |   8 +-<br>
 dts/framework/settings.py                     |  12 +-<br>
 dts/framework/test_run.py                     |  52 +++-<br>
 .../traffic_generator/__init__.py             |  13 +-<br>
 .../testbed_model/traffic_generator/scapy.py  |  14 +-<br>
 .../traffic_generator/traffic_generator.py    |  22 ++<br>
 .../testbed_model/traffic_generator/trex.py   | 259 ++++++++++++++++++<br>
 15 files changed, 440 insertions(+), 46 deletions(-)<br>
 rename dts/{ => configurations}/nodes.example.yaml (100%)<br>
 rename dts/{ => configurations}/test_run.example.yaml (88%)<br>
 rename dts/{ => configurations}/tests_config.example.yaml (100%)<br>
 create mode 100644 dts/framework/testbed_model/traffic_generator/trex.py<br>
<br>
diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst<br>
index 25c08c6a00..73d89eb1f6 100644<br>
--- a/doc/guides/tools/dts.rst<br>
+++ b/doc/guides/tools/dts.rst<br>
@@ -209,7 +209,8 @@ These need to be set up on a Traffic Generator Node:<br>
 #. **Traffic generator dependencies**<br>
<br>
    The traffic generator running on the traffic generator node must be installed beforehand.<br>
-   For Scapy traffic generator, only a few Python libraries need to be installed:<br>
+<br>
+   For Scapy traffic generator (functional tests), only a few Python libraries need to be installed:<br>
<br>
    .. code-block:: console<br>
<br>
@@ -217,6 +218,32 @@ These need to be set up on a Traffic Generator Node:<br>
       sudo pip install --upgrade pip<br>
       sudo pip install scapy==2.5.0<br>
<br>
+   For TREX traffic generator (performance tests), TREX must be downloaded and a TREX config produced for each TG NIC. For example:<br>
+<br>
+   .. code-block:: console<br>
+<br>
+      wget <a href="https://trex-tgn.cisco.com/trex/release/v3.03.tar.gz" rel="noreferrer" target="_blank">https://trex-tgn.cisco.com/trex/release/v3.03.tar.gz</a><br>
+      tar -xf v3.03.tar.gz<br>
+      cd v3.03<br>
+      sudo ./dpdk_setup_ports.py -i<br>
+<br>
+   Within the dpdk_setup_ports.py utility, follow these instructions:<br>
+     - Select MAC based config<br>
+     - Select interfaces 0 and 1 on your TG NIC<br>
+     - Do not change assumed dest to DUT MAC (just leave the default loopback)<br>
+     - Print preview of the config<br>
+     - Check for device address correctness<br>
+     - Check for socket and CPU correctness (CPU/socket NUMA node should match NIC NUMA node)<br>
+     - Write the file to a path on your system<br>
+<br>
+   Then, presuming you are using the test_run.example.yaml as a template for your test_run config:<br>
+     - Uncomment the performance_traffic_generator section, making DTS use a performance TG<br>
+     - Update the remote_path and config fields to the remote path of your TREX directory and the path to your new TREX config file<br>
+     - Update the "perf" field to enable performance testing<br>
+<br>
+   After these steps, you should be ready to run performance tests with TREX.<br>
+<br>
+<br>
 #. **Hardware dependencies**<br>
<br>
    The traffic generators, like DPDK, need a proper driver and firmware.<br>
@@ -249,9 +276,9 @@ DTS configuration is split into nodes and a test run,<br>
 and must respect the model definitions<br>
 as documented in the DTS API docs under the ``config`` page.<br>
 The root of the configuration is represented by the ``Configuration`` model.<br>
-By default, DTS will try to use the ``dts/test_run.example.yaml``<br>
+By default, DTS will try to use the ``dts/configurations/test_run.example.yaml``<br>
 :ref:`config file <test_run_configuration_example>`,<br>
-and ``dts/nodes.example.yaml``<br>
+and ``dts/configurations/nodes.example.yaml``<br>
 :ref:`config file <nodes_configuration_example>`<br>
 which are templates that illustrate what can be configured in DTS.<br>
<br>
@@ -278,9 +305,9 @@ DTS is run with ``main.py`` located in the ``dts`` directory using the ``poetry<br>
    options:<br>
      -h, --help            show this help message and exit<br>
      --test-run-config-file FILE_PATH<br>
-                           [DTS_TEST_RUN_CFG_FILE] The configuration file that describes the test cases and DPDK build options. (default: test-run.conf.yaml)<br>
+                           [DTS_TEST_RUN_CFG_FILE] The configuration file that describes the test cases and DPDK build options. (default: configurations/test_run.yaml)<br>
      --nodes-config-file FILE_PATH<br>
-                           [DTS_NODES_CFG_FILE] The configuration file that describes the SUT and TG nodes. (default: nodes.conf.yaml)<br>
+                           [DTS_NODES_CFG_FILE] The configuration file that describes the SUT and TG nodes. (default: configurations/nodes.yaml)<br>
      --tests-config-file FILE_PATH<br>
                            [DTS_TESTS_CFG_FILE] Configuration file used to override variable values inside specific test suites. (default: None)<br>
      --output-dir DIR_PATH, --output DIR_PATH<br>
@@ -549,20 +576,20 @@ And they both have two network ports which are physically connected to each othe<br>
<br>
 .. _test_run_configuration_example:<br>
<br>
-``dts/test_run.example.yaml``<br>
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
+``dts/configurations/test_run.example.yaml``<br>
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
<br>
-.. literalinclude:: ../../../dts/test_run.example.yaml<br>
+.. literalinclude:: ../../../dts/configurations/test_run.example.yaml<br>
    :language: yaml<br>
    :start-at: # Define<br>
<br>
 .. _nodes_configuration_example:<br>
<br>
<br>
-``dts/nodes.example.yaml``<br>
-~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
+``dts/configurations/nodes.example.yaml``<br>
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
<br>
-.. literalinclude:: ../../../dts/nodes.example.yaml<br>
+.. literalinclude:: ../../../dts/configurations/nodes.example.yaml<br>
    :language: yaml<br>
    :start-at: # Define<br>
<br>
@@ -575,9 +602,9 @@ to demonstrate custom test suite configuration:<br>
<br>
 .. _tests_config_example:<br>
<br>
-``dts/tests_config.example.yaml``<br>
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
+``dts/configurations/tests_config.example.yaml``<br>
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
<br>
-.. literalinclude:: ../../../dts/tests_config.example.yaml<br>
+.. literalinclude:: ../../../dts/configurations/tests_config.example.yaml<br>
    :language: yaml<br>
    :start-at: # Define<br>
diff --git a/dts/api/packet.py b/dts/api/packet.py<br>
index b6759d4ce0..ac7f64dd17 100644<br>
--- a/dts/api/packet.py<br>
+++ b/dts/api/packet.py<br>
@@ -85,9 +85,9 @@ def send_packets_and_capture(<br>
     )<br>
<br>
     assert isinstance(<br>
-        get_ctx().tg, CapturingTrafficGenerator<br>
+        get_ctx().func_tg, CapturingTrafficGenerator<br>
     ), "Cannot capture with a non-capturing traffic generator"<br>
-    tg: CapturingTrafficGenerator = cast(CapturingTrafficGenerator, get_ctx().tg)<br>
+    tg: CapturingTrafficGenerator = cast(CapturingTrafficGenerator, get_ctx().func_tg)<br>
     # TODO: implement @requires for types of traffic generator<br>
     packets = adjust_addresses(packets)<br>
     return tg.send_packets_and_capture(<br>
@@ -108,7 +108,7 @@ def send_packets(<br>
         packets: Packets to send.<br>
     """<br>
     packets = adjust_addresses(packets)<br>
-    get_ctx().tg.send_packets(packets, get_ctx().topology.tg_port_egress)<br>
+    get_ctx().func_tg.send_packets(packets, get_ctx().topology.tg_port_egress)<br>
<br>
<br>
 def get_expected_packets(<br>
diff --git a/dts/nodes.example.yaml b/dts/configurations/nodes.example.yaml<br>
similarity index 100%<br>
rename from dts/nodes.example.yaml<br>
rename to dts/configurations/nodes.example.yaml<br>
diff --git a/dts/test_run.example.yaml b/dts/configurations/test_run.example.yaml<br>
similarity index 88%<br>
rename from dts/test_run.example.yaml<br>
rename to dts/configurations/test_run.example.yaml<br>
index c90de9d68d..c8035fccf0 100644<br>
--- a/dts/test_run.example.yaml<br>
+++ b/dts/configurations/test_run.example.yaml<br>
@@ -23,8 +23,12 @@ dpdk:<br>
     # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options`<br>
     # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be<br>
     # defined, but not both.<br>
-traffic_generator:<br>
+func_traffic_generator:<br>
   type: SCAPY<br>
+# perf_traffic_generator:<br>
+#   type: TREX<br>
+#   remote_path: "/opt/trex/v3.03" # The remote path of the traffic generator application.<br>
+#   config: "/opt/trex_config/trex_config.yaml" # Additional configuration files. (Leave blank if not required)<br>
 perf: false # disable performance testing<br>
 func: true # enable functional testing<br>
 use_virtual_functions: false # use virtual functions (VFs) instead of physical functions<br>
diff --git a/dts/tests_config.example.yaml b/dts/configurations/tests_config.example.yaml<br>
similarity index 100%<br>
rename from dts/tests_config.example.yaml<br>
rename to dts/configurations/tests_config.example.yaml<br>
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py<br>
index 71b3755d6e..68db862cea 100644<br>
--- a/dts/framework/config/test_run.py<br>
+++ b/dts/framework/config/test_run.py<br>
@@ -16,7 +16,7 @@<br>
 from enum import Enum, auto, unique<br>
 from functools import cached_property<br>
 from pathlib import Path, PurePath<br>
-from typing import Annotated, Any, Literal, NamedTuple<br>
+from typing import Annotated, Any, Literal, NamedTuple, Optional<br>
<br>
 from pydantic import (<br>
     BaseModel,<br>
@@ -396,6 +396,8 @@ class TrafficGeneratorType(str, Enum):<br>
<br>
     #:<br>
     SCAPY = "SCAPY"<br>
+    #:<br>
+    TREX = "TREX"<br>
<br>
<br>
 class TrafficGeneratorConfig(FrozenModel):<br>
@@ -412,8 +414,18 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig):<br>
     type: Literal[TrafficGeneratorType.SCAPY]<br>
<br>
<br>
+class TrexTrafficGeneratorConfig(TrafficGeneratorConfig):<br>
+    """TREX traffic generator specific configuration."""<br>
+<br>
+    type: Literal[TrafficGeneratorType.TREX]<br>
+    remote_path: PurePath<br>
+    config: PurePath<br>
+<br>
+<br>
 #: A union type discriminating traffic generators by the `type` field.<br>
-TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")]<br>
+TrafficGeneratorConfigTypes = Annotated[<br>
+    TrexTrafficGeneratorConfig, ScapyTrafficGeneratorConfig, Field(discriminator="type")<br>
+]<br>
<br>
 #: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores.<br>
 LogicalCores = Annotated[<br>
@@ -461,8 +473,10 @@ class TestRunConfiguration(FrozenModel):<br>
<br>
     #: The DPDK configuration used to test.<br>
     dpdk: DPDKConfiguration<br>
-    #: The traffic generator configuration used to test.<br>
-    traffic_generator: TrafficGeneratorConfigTypes<br>
+    #: The traffic generator configuration used for functional tests.<br>
+    func_traffic_generator: Optional[ScapyTrafficGeneratorConfig] = None<br>
+    #: The traffic generator configuration used for performance tests.<br>
+    perf_traffic_generator: Optional[TrexTrafficGeneratorConfig] = None<br>
     #: Whether to run performance tests.<br>
     perf: bool<br>
     #: Whether to run functional tests.<br>
diff --git a/dts/framework/context.py b/dts/framework/context.py<br>
index ae319d949f..8f1021dc96 100644<br>
--- a/dts/framework/context.py<br>
+++ b/dts/framework/context.py<br>
@@ -6,7 +6,7 @@<br>
 import functools<br>
 from collections.abc import Callable<br>
 from dataclasses import MISSING, dataclass, field, fields<br>
-from typing import TYPE_CHECKING, Any, ParamSpec, Union<br>
+from typing import TYPE_CHECKING, Any, Optional, ParamSpec, Union<br>
<br>
 from framework.exception import InternalError<br>
 from framework.remote_session.shell_pool import ShellPool<br>
@@ -76,7 +76,8 @@ class Context:<br>
     topology: Topology<br>
     dpdk_build: "DPDKBuildEnvironment"<br>
     dpdk: "DPDKRuntimeEnvironment"<br>
-    tg: "TrafficGenerator"<br>
+    func_tg: Optional["TrafficGenerator"]<br>
+    perf_tg: Optional["TrafficGenerator"]<br>
     local: LocalContext = field(default_factory=LocalContext)<br>
     shell_pool: ShellPool = field(default_factory=ShellPool)<br>
<br>
diff --git a/dts/framework/remote_session/blocking_app.py b/dts/framework/remote_session/blocking_app.py<br>
index 8de536c259..c3b02dcc62 100644<br>
--- a/dts/framework/remote_session/blocking_app.py<br>
+++ b/dts/framework/remote_session/blocking_app.py<br>
@@ -48,20 +48,23 @@ class BlockingApp(InteractiveShell, Generic[P]):<br>
     def __init__(<br>
         self,<br>
         node: Node,<br>
-        path: PurePath,<br>
+        path: str | PurePath,<br>
         name: str | None = None,<br>
         privileged: bool = False,<br>
         app_params: P | str = "",<br>
+        add_to_shell_pool: bool = True,<br>
     ) -> None:<br>
         """Constructor.<br>
<br>
         Args:<br>
             node: The node to run the app on.<br>
-            path: Path to the application on the node.<br>
+            path: Path to the application on the node.s<br>
             name: Name to identify this application.<br>
             privileged: Run as privileged user.<br>
             app_params: The application parameters. Can be of any type inheriting :class:`Params` or<br>
                 a plain string.<br>
+            add_to_shell_pool: If :data:`True`, the blocking app's shell will be added to the<br>
+                shell pool.<br>
         """<br>
         if isinstance(app_params, str):<br>
             params = Params()<br>
@@ -69,11 +72,12 @@ def __init__(<br>
             app_params = cast(P, params)<br>
<br>
         self._path = path<br>
+        self._add_to_shell_pool = add_to_shell_pool<br>
<br>
         super().__init__(node, name, privileged, app_params)<br>
<br>
     @property<br>
-    def path(self) -> PurePath:<br>
+    def path(self) -> str | PurePath:<br>
         """The path of the DPDK app relative to the DPDK build folder."""<br>
         return self._path<br>
<br>
@@ -86,7 +90,7 @@ def wait_until_ready(self, end_token: str) -> Self:<br>
         Returns:<br>
             Itself.<br>
         """<br>
-        self.start_application(end_token)<br>
+        self.start_application(end_token, self._add_to_shell_pool)<br>
         return self<br>
<br>
     def close(self) -> None:<br>
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py<br>
index ce93247051..a65cbce209 100644<br>
--- a/dts/framework/remote_session/interactive_shell.py<br>
+++ b/dts/framework/remote_session/interactive_shell.py<br>
@@ -140,7 +140,7 @@ def _make_start_command(self) -> str:<br>
             start_command = self._node.main_session._get_privileged_command(start_command)<br>
         return start_command<br>
<br>
-    def start_application(self, prompt: str | None = None) -> None:<br>
+    def start_application(self, prompt: str | None = None, add_to_shell_pool: bool = True) -> None:<br>
         """Starts a new interactive application based on the path to the app.<br>
<br>
         This method is often overridden by subclasses as their process for starting may look<br>
@@ -151,6 +151,7 @@ def start_application(self, prompt: str | None = None) -> None:<br>
         Args:<br>
             prompt: When starting up the application, expect this string at the end of stdout when<br>
                 the application is ready. If :data:`None`, the class' default prompt will be used.<br>
+            add_to_shell_pool: If :data:`True`, the shell will be registered to the shell pool.<br>
<br>
         Raises:<br>
             InteractiveCommandExecutionError: If the application fails to start within the allotted<br>
@@ -174,7 +175,8 @@ def start_application(self, prompt: str | None = None) -> None:<br>
             self.is_alive = False  # update state on failure to start<br>
             raise InteractiveCommandExecutionError("Failed to start application.")<br>
         self._ssh_channel.settimeout(self._timeout)<br>
-        get_ctx().shell_pool.register_shell(self)<br>
+        if add_to_shell_pool:<br>
+            get_ctx().shell_pool.register_shell(self)<br>
<br>
     def send_command(<br>
         self, command: str, prompt: str | None = None, skip_first_line: bool = False<br>
@@ -259,7 +261,7 @@ def close(self) -> None:<br>
<br>
     @property<br>
     @abstractmethod<br>
-    def path(self) -> PurePath:<br>
+    def path(self) -> str | PurePath:<br>
         """Path to the shell executable."""<br>
<br>
     def _make_real_path(self) -> PurePath:<br>
diff --git a/dts/framework/settings.py b/dts/framework/settings.py<br>
index 84b627a06a..b08373b7ea 100644<br>
--- a/dts/framework/settings.py<br>
+++ b/dts/framework/settings.py<br>
@@ -130,11 +130,17 @@ class Settings:<br>
     """<br>
<br>
     #:<br>
-    test_run_config_path: Path = Path(__file__).parent.parent.joinpath("test_run.yaml")<br>
+    test_run_config_path: Path = Path(__file__).parent.parent.joinpath(<br>
+        "configurations/test_run.yaml"<br>
+    )<br>
     #:<br>
-    nodes_config_path: Path = Path(__file__).parent.parent.joinpath("nodes.yaml")<br>
+    nodes_config_path: Path = Path(__file__).parent.parent.joinpath("configurations/nodes.yaml")<br>
     #:<br>
-    tests_config_path: Path | None = None<br>
+    tests_config_path: Path | None = (<br>
+        Path(__file__).parent.parent.joinpath("configurations/tests_config.yaml")<br>
+        if os.path.exists("configurations/tests_config.yaml")<br>
+        else None<br>
+    )<br>
     #:<br>
     output_dir: str = "output"<br>
     #:<br>
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py<br>
index 9cf04c0b06..ff0a12c9ce 100644<br>
--- a/dts/framework/test_run.py<br>
+++ b/dts/framework/test_run.py<br>
@@ -113,7 +113,7 @@<br>
 from framework.remote_session.dpdk import DPDKBuildEnvironment, DPDKRuntimeEnvironment<br>
 from framework.settings import SETTINGS<br>
 from framework.test_result import Result, ResultNode, TestRunResult<br>
-from framework.test_suite import BaseConfig, TestCase, TestSuite<br>
+from framework.test_suite import BaseConfig, TestCase, TestCaseType, TestSuite<br>
 from framework.testbed_model.capability import (<br>
     Capability,<br>
     get_supported_capabilities,<br>
@@ -199,10 +199,26 @@ def __init__(<br>
<br>
         dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)<br>
         dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)<br>
-        traffic_generator = create_traffic_generator(config.traffic_generator, tg_node)<br>
+<br>
+        func_traffic_generator = (<br>
+            create_traffic_generator(config.func_traffic_generator, tg_node)<br>
+            if config.func and config.func_traffic_generator<br>
+            else None<br>
+        )<br>
+        perf_traffic_generator = (<br>
+            create_traffic_generator(config.perf_traffic_generator, tg_node)<br>
+            if config.perf and config.perf_traffic_generator<br>
+            else None<br>
+        )<br>
<br>
         self.ctx = Context(<br>
-            sut_node, tg_node, topology, dpdk_build_env, dpdk_runtime_env, traffic_generator<br>
+            sut_node,<br>
+            tg_node,<br>
+            topology,<br>
+            dpdk_build_env,<br>
+            dpdk_runtime_env,<br>
+            func_traffic_generator,<br>
+            perf_traffic_generator,<br>
         )<br>
         self.result = result<br>
         self.selected_tests = list(self.config.filter_tests(tests_config))<br>
@@ -335,7 +351,10 @@ def next(self) -> State | None:<br>
             test_run.ctx.topology.instantiate_vf_ports()<br>
<br>
         test_run.ctx.topology.configure_ports("sut", "dpdk")<br>
-        test_run.ctx.tg.setup(test_run.ctx.topology)<br>
+        if test_run.ctx.func_tg:<br>
+            test_run.ctx.func_tg.setup(test_run.ctx.topology)<br>
+        if test_run.ctx.perf_tg:<br>
+            test_run.ctx.perf_tg.setup(test_run.ctx.topology)<br>
<br>
         self.result.ports = [<br>
             port.to_dict()<br>
@@ -425,7 +444,10 @@ def next(self) -> State | None:<br>
             self.test_run.ctx.topology.delete_vf_ports()<br>
<br>
         self.test_run.ctx.shell_pool.terminate_current_pool()<br>
-        self.test_run.ctx.tg.teardown()<br>
+        if self.test_run.ctx.func_tg and self.test_run.ctx.func_tg.is_setup:<br>
+            self.test_run.ctx.func_tg.teardown()<br>
+        if self.test_run.ctx.perf_tg and self.test_run.ctx.perf_tg.is_setup:<br>
+            self.test_run.ctx.perf_tg.teardown()<br>
         self.test_run.ctx.topology.teardown()<br>
         self.test_run.ctx.dpdk.teardown()<br>
         self.test_run.ctx.tg_node.teardown()<br>
@@ -611,6 +633,26 @@ def next(self) -> State | None:<br>
         )<br>
         self.test_run.ctx.topology.configure_ports("sut", sut_ports_drivers)<br>
<br>
+        if (<br>
+            self.test_run.ctx.perf_tg<br>
+            and self.test_run.ctx.perf_tg.is_setup<br>
+            and self.test_case.test_type is TestCaseType.FUNCTIONAL<br>
+        ):<br>
+            self.test_run.ctx.perf_tg.teardown()<br>
+            self.test_run.ctx.topology.configure_ports("tg", "kernel")<br>
+            if self.test_run.ctx.func_tg and not self.test_run.ctx.func_tg.is_setup:<br>
+                self.test_run.ctx.func_tg.setup(self.test_run.ctx.topology)<br>
+<br>
+        if (<br>
+            self.test_run.ctx.func_tg<br>
+            and self.test_run.ctx.func_tg.is_setup<br>
+            and self.test_case.test_type is TestCaseType.PERFORMANCE<br>
+        ):<br>
+            self.test_run.ctx.func_tg.teardown()<br>
+            self.test_run.ctx.topology.configure_ports("tg", "dpdk")<br>
+            if self.test_run.ctx.perf_tg and not self.test_run.ctx.perf_tg.is_setup:<br>
+                self.test_run.ctx.perf_tg.setup(self.test_run.ctx.topology)<br>
+<br>
         self.test_suite.set_up_test_case()<br>
         self.result.mark_step_as("setup", Result.PASS)<br>
         return TestCaseExecution(<br>
diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py<br>
index 2a259a6e6c..fca251f534 100644<br>
--- a/dts/framework/testbed_model/traffic_generator/__init__.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/__init__.py<br>
@@ -14,17 +14,22 @@<br>
 and a capturing traffic generator is required.<br>
 """<br>
<br>
-from framework.config.test_run import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig<br>
+from framework.config.test_run import (<br>
+    ScapyTrafficGeneratorConfig,<br>
+    TrafficGeneratorConfig,<br>
+    TrexTrafficGeneratorConfig,<br>
+)<br>
 from framework.exception import ConfigurationError<br>
 from framework.testbed_model.node import Node<br>
<br>
-from .capturing_traffic_generator import CapturingTrafficGenerator<br>
 from .scapy import ScapyTrafficGenerator<br>
+from .traffic_generator import TrafficGenerator<br>
+from .trex import TrexTrafficGenerator<br>
<br>
<br>
 def create_traffic_generator(<br>
     traffic_generator_config: TrafficGeneratorConfig, node: Node<br>
-) -> CapturingTrafficGenerator:<br>
+) -> TrafficGenerator:<br>
     """The factory function for creating traffic generator objects from the test run configuration.<br>
<br>
     Args:<br>
@@ -40,5 +45,7 @@ def create_traffic_generator(<br>
     match traffic_generator_config:<br>
         case ScapyTrafficGeneratorConfig():<br>
             return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True)<br>
+        case TrexTrafficGeneratorConfig():<br>
+            return TrexTrafficGenerator(node, traffic_generator_config)<br>
         case _:<br>
             raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}")<br>
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py<br>
index a31807e8e4..9e15a31c00 100644<br>
--- a/dts/framework/testbed_model/traffic_generator/scapy.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py<br>
@@ -170,12 +170,17 @@ def stop_capturing_and_collect(<br>
         finally:<br>
             self.stop_capturing()<br>
<br>
-    def start_application(self, prompt: str | None = None) -> None:<br>
+    def start_application(self, prompt: str | None = None, add_to_shell_pool: bool = True) -> None:<br>
         """Overrides :meth:`framework.remote_session.interactive_shell.start_application`.<br>
<br>
         Prepares the Python shell for scapy and starts the sniffing in a new thread.<br>
+<br>
+        Args:<br>
+            prompt: When starting up the application, expect this string at the end of stdout when<br>
+                the application is ready. If :data:`None`, the class' default prompt will be used.<br>
+            add_to_shell_pool: If :data:`True`, the shell will be registered to the shell pool.<br>
         """<br>
-        super().start_application(prompt)<br>
+        super().start_application(prompt, add_to_shell_pool)<br>
         self.send_command("from scapy.all import *")<br>
         self._sniffer.start()<br>
         self._is_sniffing.wait()<br>
@@ -320,15 +325,16 @@ def setup(self, topology: Topology) -> None:<br>
<br>
         Binds the TG node ports to the kernel drivers and starts up the async sniffer.<br>
         """<br>
+        super().setup(topology)<br>
         topology.configure_ports("tg", "kernel")<br>
<br>
         self._sniffer = ScapyAsyncSniffer(<br>
             self._tg_node, topology.tg_port_ingress, self._sniffer_name<br>
         )<br>
-        self._sniffer.start_application()<br>
+        self._sniffer.start_application(add_to_shell_pool=False)<br>
<br>
         self._shell = PythonShell(self._tg_node, "scapy", privileged=True)<br>
-        self._shell.start_application()<br>
+        self._shell.start_application(add_to_shell_pool=False)<br>
         self._shell.send_command("from scapy.all import *")<br>
         self._shell.send_command("from scapy.contrib.lldp import *")<br>
<br>
diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
index e5f246df7a..cdda5a7c08 100644<br>
--- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
+++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py<br>
@@ -11,9 +11,12 @@<br>
 from abc import ABC, abstractmethod<br>
 from typing import Any<br>
<br>
+from scapy.packet import Packet<br>
+<br>
 from framework.config.test_run import TrafficGeneratorConfig<br>
 from framework.logger import DTSLogger, get_dts_logger<br>
 from framework.testbed_model.node import Node<br>
+from framework.testbed_model.port import Port<br>
 from framework.testbed_model.topology import Topology<br>
<br>
<br>
@@ -30,6 +33,7 @@ class TrafficGenerator(ABC):<br>
     _config: TrafficGeneratorConfig<br>
     _tg_node: Node<br>
     _logger: DTSLogger<br>
+    _is_setup: bool<br>
<br>
     def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs: Any) -> None:<br>
         """Initialize the traffic generator.<br>
@@ -45,12 +49,25 @@ def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs: Any)<br>
         self._config = config<br>
         self._tg_node = tg_node<br>
         self._logger = get_dts_logger(f"{self._<a href="http://tg_node.name" rel="noreferrer" target="_blank">tg_node.name</a>} {self._config.type}")<br>
+        self._is_setup = False<br>
+<br>
+    def send_packets(self, packets: list[Packet], port: Port) -> None:<br>
+        """Send `packets` and block until they are fully sent.<br>
+<br>
+        Send `packets` on `port`, then wait until `packets` are fully sent.<br>
+<br>
+        Args:<br>
+            packets: The packets to send.<br>
+            port: The egress port on the TG node.<br>
+        """<br>
<br>
     def setup(self, topology: Topology) -> None:<br>
         """Setup the traffic generator."""<br>
+        self._is_setup = True<br>
<br>
     def teardown(self) -> None:<br>
         """Teardown the traffic generator."""<br>
+        self._is_setup = False<br>
         self.close()<br>
<br>
     @property<br>
@@ -61,3 +78,8 @@ def is_capturing(self) -> bool:<br>
     @abstractmethod<br>
     def close(self) -> None:<br>
         """Free all resources used by the traffic generator."""<br>
+<br>
+    @property<br>
+    def is_setup(self) -> bool:<br>
+        """Indicates whether the traffic generator application is currently running."""<br>
+        return self._is_setup<br>
diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py<br>
new file mode 100644<br>
index 0000000000..6ae6d1f181<br>
--- /dev/null<br>
+++ b/dts/framework/testbed_model/traffic_generator/trex.py<br>
@@ -0,0 +1,259 @@<br>
+# SPDX-License-Identifier: BSD-3-Clause<br>
+# Copyright(c) 2025 University of New Hampshire<br>
+<br>
+"""Implementation for TREX performance traffic generator."""<br>
+<br>
+import ast<br>
+import time<br>
+from dataclasses import dataclass, field<br>
+from enum import auto<br>
+from typing import ClassVar<br>
+<br>
+from scapy.packet import Packet<br>
+<br>
+from framework.config.node import OS, NodeConfiguration<br>
+from framework.config.test_run import TrexTrafficGeneratorConfig<br>
+from framework.parser import TextParser<br>
+from framework.remote_session.blocking_app import BlockingApp<br>
+from framework.remote_session.python_shell import PythonShell<br>
+from framework.testbed_model.node import Node, create_session<br>
+from framework.testbed_model.os_session import OSSession<br>
+from framework.testbed_model.topology import Topology<br>
+from framework.testbed_model.traffic_generator.performance_traffic_generator import (<br>
+    PerformanceTrafficGenerator,<br>
+    PerformanceTrafficStats,<br>
+)<br>
+from framework.utils import StrEnum<br>
+<br>
+<br>
+@dataclass(slots=True)<br>
+class TrexPerformanceTrafficStats(PerformanceTrafficStats, TextParser):<br>
+    """Data structure to store performance statistics for a given test run.<br>
+<br>
+    This class overrides the initialization of :class:`PerformanceTrafficStats`<br>
+    in order to set the attribute values using the TREX stats output.<br>
+<br>
+    Attributes:<br>
+        tx_pps: Recorded tx packets per second.<br>
+        tx_bps: Recorded tx bytes per second.<br>
+        rx_pps: Recorded rx packets per second.<br>
+        rx_bps: Recorded rx bytes per second.<br>
+        frame_size: The total length of the frame.<br>
+    """<br>
+<br>
+    tx_pps: int = field(metadata=TextParser.find_int(r"total.*'tx_pps': (\d+)"))<br>
+    tx_bps: int = field(metadata=TextParser.find_int(r"total.*'tx_bps': (\d+)"))<br>
+    rx_pps: int = field(metadata=TextParser.find_int(r"total.*'rx_pps': (\d+)"))<br>
+    rx_bps: int = field(metadata=TextParser.find_int(r"total.*'rx_bps': (\d+)"))<br>
+<br>
+<br>
+class TrexStatelessTXModes(StrEnum):<br>
+    """Flags indicating TREX instance's current transmission mode."""<br>
+<br>
+    #: Transmit continuously<br>
+    STLTXCont = auto()<br>
+    #: Transmit in a single burst<br>
+    STLTXSingleBurst = auto()<br>
+    #: Transmit in multiple bursts<br>
+    STLTXMultiBurst = auto()<br>
+<br>
+<br>
+class TrexTrafficGenerator(PerformanceTrafficGenerator):<br>
+    """TREX traffic generator.<br>
+<br>
+    This implementation leverages the stateless API library provided in the TREX installation.<br>
+<br>
+    Attributes:<br>
+        stl_client_name: The name of the stateless client used in the stateless API.<br>
+        packet_stream_name: The name of the stateless packet stream used in the stateless API.<br>
+    """<br>
+<br>
+    _os_session: OSSession<br>
+<br>
+    _tg_config: TrexTrafficGeneratorConfig<br>
+    _node_config: NodeConfiguration<br>
+<br>
+    _shell: PythonShell<br>
+    _python_indentation: ClassVar[str] = " " * 4<br>
+<br>
+    stl_client_name: ClassVar[str] = "client"<br>
+    packet_stream_name: ClassVar[str] = "stream"<br>
+<br>
+    _streaming_mode: TrexStatelessTXModes = TrexStatelessTXModes.STLTXCont<br>
+<br>
+    _tg_cores: int = 10<br>
+<br>
+    _trex_app: BlockingApp<br>
+<br>
+    def __init__(self, tg_node: Node, config: TrexTrafficGeneratorConfig) -> None:<br>
+        """Initialize the TREX server.<br>
+<br>
+        Initializes needed OS sessions for the creation of the TREX server process.<br>
+<br>
+        Args:<br>
+            tg_node: TG node the TREX instance is operating on.<br>
+            config: Traffic generator config provided for TREX instance.<br>
+        """<br>
+        assert (<br>
+            tg_node.config.os == OS.linux<br>
+        ), "Linux is the only supported OS for trex traffic generation"<br>
+<br>
+        super().__init__(tg_node=tg_node, config=config)<br>
+        self._tg_node_config = tg_node.config<br>
+        self._tg_config = config<br>
+<br>
+        self._os_session = create_session(self._tg_node.config, "TREX", self._logger)<br>
+<br>
+    def setup(self, topology: Topology):<br>
+        """Initialize and start a TREX server process."""<br>
+        super().setup(topology)<br>
+<br>
+        self._shell = PythonShell(self._tg_node, "TREX-client", privileged=True)<br>
+<br>
+        # Start TREX server process.<br>
+        trex_app_path = f"cd {self._tg_config.remote_path} && ./t-rex-64"<br>
+        self._trex_app = BlockingApp(<br>
+            node=self._tg_node,<br>
+            path=trex_app_path,<br>
+            name="trex-tg",<br>
+            privileged=True,<br>
+            app_params=f"--cfg {self._tg_config.config} -c {self._tg_cores} -i",<br>
+            add_to_shell_pool=False,<br>
+        )<br>
+        self._trex_app.wait_until_ready("-Per port stats table")<br>
+<br>
+        self._shell.start_application()<br>
+        self._shell.send_command("import os")<br>
+        self._shell.send_command(<br>
+            f"os.chdir('{self._tg_config.remote_path}/automation/trex_control_plane/interactive')"<br>
+        )<br>
+<br>
+        # Import stateless API components.<br>
+        imports = [<br>
+            "import trex",<br>
+            "import trex.stl",<br>
+            "import trex.stl.trex_stl_client",<br>
+            "import trex.stl.trex_stl_streams",<br>
+            "import trex.stl.trex_stl_packet_builder_scapy",<br>
+            "from scapy.layers.l2 import Ether",<br>
+            "from scapy.layers.inet import IP",<br>
+            "from scapy.packet import Raw",<br>
+        ]<br>
+        self._shell.send_command("\n".join(imports))<br>
+<br>
+        stateless_client = [<br>
+            f"{self.stl_client_name} = trex.stl.trex_stl_client.STLClient(",<br>
+            f"username='{self._tg_node_config.user}',",<br>
+            "server='127.0.0.1',",<br>
+            ")",<br>
+        ]<br>
+<br>
+        self._shell.send_command(f"\n{self._python_indentation}".join(stateless_client))<br>
+        self._shell.send_command(f"{self.stl_client_name}.connect()")<br>
+<br>
+    def calculate_traffic_and_stats(<br>
+        self,<br>
+        packet: Packet,<br>
+        duration: float,<br>
+        send_mpps: int | None = None,<br>
+    ) -> PerformanceTrafficStats:<br>
+        """Send packet traffic and acquire associated statistics.<br>
+<br>
+        Overrides<br>
+        :meth:`~.traffic_generator.PerformanceTrafficGenerator.calculate_traffic_and_stats`.<br>
+        """<br>
+        trex_stats_output = ast.literal_eval(self._generate_traffic(packet, duration, send_mpps))<br>
+        stats = TrexPerformanceTrafficStats.parse(str(trex_stats_output))<br>
+        stats.frame_size = len(packet)<br>
+        return stats<br>
+<br>
+    def _generate_traffic(<br>
+        self, packet: Packet, duration: float, send_mpps: int | None = None<br>
+    ) -> str:<br>
+        """Generate traffic using provided packet.<br>
+<br>
+        Uses the provided packet to generate traffic for the provided duration.<br>
+<br>
+        Args:<br>
+            packet: The packet being used for the performance test.<br>
+            duration: The duration of the test being performed.<br>
+            send_mpps: MPPS send rate.<br>
+<br>
+        Returns:<br>
+            A string output of statistics provided by the traffic generator.<br>
+        """<br>
+        self._create_packet_stream(packet)<br>
+        self._setup_trex_client()<br>
+<br>
+        stats = self._send_traffic_and_get_stats(duration, send_mpps)<br>
+<br>
+        return stats<br>
+<br>
+    def _setup_trex_client(self) -> None:<br>
+        """Create trex client and connect to the server process."""<br>
+        # Prepare TREX client for next performance test.<br>
+        procedure = [<br>
+            f"{self.stl_client_name}.connect()",<br>
+            f"{self.stl_client_name}.reset(ports = [0, 1])",<br>
+            f"{self.stl_client_name}.clear_stats()",<br>
+            f"{self.stl_client_name}.add_streams({self.packet_stream_name}, ports=[0, 1])",<br>
+        ]<br>
+<br>
+        for command in procedure:<br>
+            self._shell.send_command(command)<br>
+<br>
+    def _create_packet_stream(self, packet: Packet) -> None:<br>
+        """Create TREX packet stream with the given packet.<br>
+<br>
+        Args:<br>
+            packet: The packet being used for the performance test.<br>
+        """<br>
+        # Create the tx packet on the TG shell<br>
+        self._shell.send_command(f"packet={packet.command()}")<br>
+<br>
+        packet_stream = [<br>
+            f"{self.packet_stream_name} = trex.stl.trex_stl_streams.STLStream(",<br>
+            f"name='Test_{len(packet)}_bytes',",<br>
+            "packet=trex.stl.trex_stl_packet_builder_scapy.STLPktBuilder(pkt=packet),",<br>
+            f"mode=trex.stl.trex_stl_streams.{self._streaming_mode}(percentage=100),",<br>
+            ")",<br>
+        ]<br>
+        self._shell.send_command("\n".join(packet_stream))<br>
+<br>
+    def _send_traffic_and_get_stats(self, duration: float, send_mpps: float | None = None) -> str:<br>
+        """Send traffic and get TG Rx stats.<br>
+<br>
+        Sends traffic from the TREX client's ports for the given duration.<br>
+        When the traffic sending duration has passed, collect the aggregate<br>
+        statistics and return TREX's global stats as a string.<br>
+<br>
+        Args:<br>
+            duration: The traffic generation duration.<br>
+            send_mpps: The millions of packets per second for TREX to send from each port.<br>
+        """<br>
+        if send_mpps:<br>
+            self._shell.send_command(f"""{self.stl_client_name}.start(ports=[0, 1],<br>
+                mult = '{send_mpps}mpps',<br>
+                duration = {duration})""")<br>
+        else:<br>
+            self._shell.send_command(f"""{self.stl_client_name}.start(ports=[0, 1],<br>
+                mult = '100%',<br>
+                duration = {duration})""")<br>
+<br>
+        time.sleep(duration)<br>
+<br>
+        stats = self._shell.send_command(<br>
+            f"{self.stl_client_name}.get_stats(ports=[0, 1])", skip_first_line=True<br>
+        )<br>
+<br>
+        self._shell.send_command(f"{self.stl_client_name}.stop(ports=[0, 1])")<br>
+<br>
+        return stats<br>
+<br>
+    def close(self) -> None:<br>
+        """Overrides :meth:`.traffic_generator.TrafficGenerator.close`.<br>
+<br>
+        Stops the traffic generator and sniffer shells.<br>
+        """<br>
+        self._trex_app.close()<br>
+        self._shell.close()<br>
-- <br>
2.49.0<br>
<br>
</blockquote></div>