<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>