DPDK patches and discussions
 help / color / mirror / Atom feed
From: Luca Vizzarro <luca.vizzarro@arm.com>
To: dev@dpdk.org
Cc: Nicholas Pratte <npratte@iol.unh.edu>,
	Dean Marx <dmarx@iol.unh.edu>,
	Luca Vizzarro <luca.vizzarro@arm.com>,
	Paul Szczepanek <paul.szczepanek@arm.com>,
	Patrick Robb <probb@iol.unh.edu>
Subject: [PATCH v2 7/7] dts: remove node distinction
Date: Wed, 12 Feb 2025 16:46:00 +0000	[thread overview]
Message-ID: <20250212164600.23759-8-luca.vizzarro@arm.com> (raw)
In-Reply-To: <20250212164600.23759-1-luca.vizzarro@arm.com>

Remove the distinction between SUT and TG nodes for configuration
purposes. As DPDK and the traffic generator belong to the runtime side
of testing, and don't belong to the testbed model, these are better
suited to be configured under the test runs.

Split the DPDK configuration in DPDKBuildConfiguration and
DPDKRuntimeConfiguration. The former stays the same but gains
implementation in its own self-contained class. DPDKRuntimeConfiguration
instead represents the prior dpdk options. Through a new
DPDKRuntimeEnvironment class all the DPDK-related runtime features are
now also self-contained. This sets a predisposition for DPDK-based
traffic generator as well, as it'd make it easier to handle their own
environment using the pre-existing functionality.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 doc/api/dts/framework.remote_session.dpdk.rst |   8 +
 doc/api/dts/framework.remote_session.rst      |   1 +
 dts/framework/config/__init__.py              |  16 +-
 dts/framework/config/node.py                  |  73 +--
 dts/framework/config/test_run.py              |  72 ++-
 dts/framework/context.py                      |  11 +-
 .../sut_node.py => remote_session/dpdk.py}    | 444 +++++++++---------
 dts/framework/remote_session/dpdk_shell.py    |  12 +-
 .../single_active_interactive_shell.py        |   4 +-
 dts/framework/runner.py                       |  16 +-
 dts/framework/test_result.py                  |   2 +-
 dts/framework/test_run.py                     |  23 +-
 dts/framework/test_suite.py                   |   9 +-
 dts/framework/testbed_model/capability.py     |  24 +-
 dts/framework/testbed_model/node.py           |  87 ++--
 dts/framework/testbed_model/tg_node.py        | 125 -----
 .../traffic_generator/__init__.py             |   8 +-
 .../testbed_model/traffic_generator/scapy.py  |  12 +-
 .../traffic_generator/traffic_generator.py    |   9 +-
 dts/tests/TestSuite_smoke_tests.py            |   6 +-
 dts/tests/TestSuite_softnic.py                |   2 +-
 21 files changed, 393 insertions(+), 571 deletions(-)
 create mode 100644 doc/api/dts/framework.remote_session.dpdk.rst
 rename dts/framework/{testbed_model/sut_node.py => remote_session/dpdk.py} (61%)
 delete mode 100644 dts/framework/testbed_model/tg_node.py

diff --git a/doc/api/dts/framework.remote_session.dpdk.rst b/doc/api/dts/framework.remote_session.dpdk.rst
new file mode 100644
index 0000000000..830364b984
--- /dev/null
+++ b/doc/api/dts/framework.remote_session.dpdk.rst
@@ -0,0 +1,8 @@
+.. SPDX-License-Identifier: BSD-3-Clause
+
+dpdk - DPDK Environments
+===========================================
+
+.. automodule:: framework.remote_session.dpdk
+   :members:
+   :show-inheritance:
diff --git a/doc/api/dts/framework.remote_session.rst b/doc/api/dts/framework.remote_session.rst
index dd6f8530d7..79d65e3444 100644
--- a/doc/api/dts/framework.remote_session.rst
+++ b/doc/api/dts/framework.remote_session.rst
@@ -15,6 +15,7 @@ remote\_session - Node Connections Package
    framework.remote_session.ssh_session
    framework.remote_session.interactive_remote_session
    framework.remote_session.interactive_shell
+   framework.remote_session.dpdk
    framework.remote_session.dpdk_shell
    framework.remote_session.testpmd_shell
    framework.remote_session.python_shell
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index 273a5cc3a7..c42eacb748 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -37,16 +37,12 @@
 from framework.exception import ConfigurationError
 
 from .common import FrozenModel, ValidationContext
-from .node import (
-    NodeConfigurationTypes,
-    SutNodeConfiguration,
-    TGNodeConfiguration,
-)
+from .node import NodeConfiguration
 from .test_run import TestRunConfiguration
 
 TestRunsConfig = Annotated[list[TestRunConfiguration], Field(min_length=1)]
 
-NodesConfig = Annotated[list[NodeConfigurationTypes], Field(min_length=1)]
+NodesConfig = Annotated[list[NodeConfiguration], Field(min_length=1)]
 
 
 class Configuration(FrozenModel):
@@ -125,10 +121,6 @@ def validate_test_runs_against_nodes(self) -> Self:
                 f"Test run {test_run_no}.system_under_test_node "
                 f"({sut_node_name}) is not a valid node name."
             )
-            assert isinstance(sut_node, SutNodeConfiguration), (
-                f"Test run {test_run_no}.system_under_test_node is a valid node name, "
-                "but it is not a valid SUT node."
-            )
 
             tg_node_name = test_run.traffic_generator_node
             tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
@@ -137,10 +129,6 @@ def validate_test_runs_against_nodes(self) -> Self:
                 f"Test run {test_run_no}.traffic_generator_name "
                 f"({tg_node_name}) is not a valid node name."
             )
-            assert isinstance(tg_node, TGNodeConfiguration), (
-                f"Test run {test_run_no}.traffic_generator_name is a valid node name, "
-                "but it is not a valid TG node."
-            )
 
         return self
 
diff --git a/dts/framework/config/node.py b/dts/framework/config/node.py
index 97e0285912..438a1bdc8f 100644
--- a/dts/framework/config/node.py
+++ b/dts/framework/config/node.py
@@ -9,8 +9,7 @@
 The root model of a node configuration is :class:`NodeConfiguration`.
 """
 
-from enum import Enum, auto, unique
-from typing import Annotated, Literal
+from enum import auto, unique
 
 from pydantic import Field, model_validator
 from typing_extensions import Self
@@ -32,14 +31,6 @@ class OS(StrEnum):
     windows = auto()
 
 
-@unique
-class TrafficGeneratorType(str, Enum):
-    """The supported traffic generators."""
-
-    #:
-    SCAPY = "SCAPY"
-
-
 class HugepageConfiguration(FrozenModel):
     r"""The hugepage configuration of :class:`~framework.testbed_model.node.Node`\s."""
 
@@ -62,33 +53,6 @@ class PortConfig(FrozenModel):
     os_driver: str = Field(examples=["i40e", "ice", "mlx5_core"])
 
 
-class TrafficGeneratorConfig(FrozenModel):
-    """A protocol required to define traffic generator types."""
-
-    #: The traffic generator type the child class is required to define to be distinguished among
-    #: others.
-    type: TrafficGeneratorType
-
-
-class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig):
-    """Scapy traffic generator specific configuration."""
-
-    type: Literal[TrafficGeneratorType.SCAPY]
-
-
-#: A union type discriminating traffic generators by the `type` field.
-TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")]
-
-#: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores.
-LogicalCores = Annotated[
-    str,
-    Field(
-        examples=["1,2,3,4,5,18-22", "10-15", "any"],
-        pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$|any",
-    ),
-]
-
-
 class NodeConfiguration(FrozenModel):
     r"""The configuration of :class:`~framework.testbed_model.node.Node`\s."""
 
@@ -118,38 +82,3 @@ def verify_unique_port_names(self) -> Self:
             )
             used_port_names[port.name] = idx
         return self
-
-
-class DPDKConfiguration(FrozenModel):
-    """Configuration of the DPDK EAL parameters."""
-
-    #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty
-    #: string or omitting this field means use any core except for the first one. The first core
-    #: will only be used if explicitly set.
-    lcores: LogicalCores = ""
-
-    #: The number of memory channels to use when running DPDK.
-    memory_channels: int = 1
-
-    @property
-    def use_first_core(self) -> bool:
-        """Returns :data:`True` if `lcores` explicitly selects the first core."""
-        return "0" in self.lcores
-
-
-class SutNodeConfiguration(NodeConfiguration):
-    """:class:`~framework.testbed_model.sut_node.SutNode` specific configuration."""
-
-    #: The runtime configuration for DPDK.
-    dpdk_config: DPDKConfiguration
-
-
-class TGNodeConfiguration(NodeConfiguration):
-    """:class:`~framework.testbed_model.tg_node.TGNode` specific configuration."""
-
-    #: The configuration of the traffic generator present on the TG node.
-    traffic_generator: TrafficGeneratorConfigTypes
-
-
-#: Union type for all the node configuration types.
-NodeConfigurationTypes = TGNodeConfiguration | SutNodeConfiguration
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index eef01d0340..1b3045730d 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -13,10 +13,10 @@
 import tarfile
 from collections import deque
 from collections.abc import Iterable
-from enum import auto, unique
+from enum import Enum, auto, unique
 from functools import cached_property
 from pathlib import Path, PurePath
-from typing import Any, Literal, NamedTuple
+from typing import Annotated, Any, Literal, NamedTuple
 
 from pydantic import Field, field_validator, model_validator
 from typing_extensions import TYPE_CHECKING, Self
@@ -361,6 +361,68 @@ def verify_distinct_nodes(self) -> Self:
         return self
 
 
+@unique
+class TrafficGeneratorType(str, Enum):
+    """The supported traffic generators."""
+
+    #:
+    SCAPY = "SCAPY"
+
+
+class TrafficGeneratorConfig(FrozenModel):
+    """A protocol required to define traffic generator types."""
+
+    #: The traffic generator type the child class is required to define to be distinguished among
+    #: others.
+    type: TrafficGeneratorType
+
+
+class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig):
+    """Scapy traffic generator specific configuration."""
+
+    type: Literal[TrafficGeneratorType.SCAPY]
+
+
+#: A union type discriminating traffic generators by the `type` field.
+TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")]
+
+#: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores.
+LogicalCores = Annotated[
+    str,
+    Field(
+        examples=["1,2,3,4,5,18-22", "10-15", "any"],
+        pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$|any",
+    ),
+]
+
+
+class DPDKRuntimeConfiguration(FrozenModel):
+    """Configuration of the DPDK EAL parameters."""
+
+    #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty
+    #: string or omitting this field means use any core except for the first one. The first core
+    #: will only be used if explicitly set.
+    lcores: LogicalCores = ""
+
+    #: The number of memory channels to use when running DPDK.
+    memory_channels: int = 1
+
+    #: The names of virtual devices to test.
+    vdevs: list[str] = Field(default_factory=list)
+
+    @property
+    def use_first_core(self) -> bool:
+        """Returns :data:`True` if `lcores` explicitly selects the first core."""
+        return "0" in self.lcores
+
+
+class DPDKConfiguration(DPDKRuntimeConfiguration):
+    """The DPDK configuration needed to test."""
+
+    #: The DPDKD build configuration used to test.
+    build: DPDKBuildConfiguration
+
+
 class TestRunConfiguration(FrozenModel):
     """The configuration of a test run.
 
@@ -369,7 +431,9 @@ class TestRunConfiguration(FrozenModel):
     """
 
     #: The DPDK configuration used to test.
-    dpdk_config: DPDKBuildConfiguration = Field(alias="dpdk_build")
+    dpdk: DPDKConfiguration
+    #: The traffic generator configuration used to test.
+    traffic_generator: TrafficGeneratorConfigTypes
     #: Whether to run performance tests.
     perf: bool
     #: Whether to run functional tests.
@@ -382,8 +446,6 @@ class TestRunConfiguration(FrozenModel):
     system_under_test_node: str
     #: The TG node name to use in this test run.
     traffic_generator_node: str
-    #: The names of virtual devices to test.
-    vdevs: list[str] = Field(default_factory=list)
     #: The seed to use for pseudo-random generation.
     random_seed: int | None = None
     #: The port links between the specified nodes to use.
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 03eaf63b88..8adffff57f 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -10,11 +10,12 @@
 from framework.exception import InternalError
 from framework.settings import SETTINGS
 from framework.testbed_model.cpu import LogicalCoreCount, LogicalCoreList
+from framework.testbed_model.node import Node
 from framework.testbed_model.topology import Topology
 
 if TYPE_CHECKING:
-    from framework.testbed_model.sut_node import SutNode
-    from framework.testbed_model.tg_node import TGNode
+    from framework.remote_session.dpdk import DPDKRuntimeEnvironment
+    from framework.testbed_model.traffic_generator.traffic_generator import TrafficGenerator
 
 P = ParamSpec("P")
 
@@ -62,9 +63,11 @@ def reset(self) -> None:
 class Context:
     """Runtime context."""
 
-    sut_node: "SutNode"
-    tg_node: "TGNode"
+    sut_node: Node
+    tg_node: Node
     topology: Topology
+    dpdk: "DPDKRuntimeEnvironment"
+    tg: "TrafficGenerator"
     local: LocalContext = field(default_factory=LocalContext)
 
 
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/remote_session/dpdk.py
similarity index 61%
rename from dts/framework/testbed_model/sut_node.py
rename to dts/framework/remote_session/dpdk.py
index 9007d89b1c..476d6915d3 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -1,47 +1,40 @@
-# SPDX-License-Identifier: BSD-3-Clause
-# Copyright(c) 2010-2014 Intel Corporation
+# SPDX-License-Identifier: BSD-3-C
 # Copyright(c) 2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2023 University of New Hampshire
-# Copyright(c) 2024 Arm Limited
+# Copyright(c) 2025 Arm Limited
 
-"""System under test (DPDK + hardware) node.
-
-A system under test (SUT) is the combination of DPDK
-and the hardware we're testing with DPDK (NICs, crypto and other devices).
-An SUT node is where this SUT runs.
-"""
+"""DPDK environment."""
 
 import os
 import time
 from collections.abc import Iterable
 from dataclasses import dataclass
+from functools import cached_property
 from pathlib import Path, PurePath
+from typing import Final
 
-from framework.config.node import (
-    SutNodeConfiguration,
-)
 from framework.config.test_run import (
     DPDKBuildConfiguration,
     DPDKBuildOptionsConfiguration,
     DPDKPrecompiledBuildConfiguration,
+    DPDKRuntimeConfiguration,
     DPDKUncompiledBuildConfiguration,
     LocalDPDKTarballLocation,
     LocalDPDKTreeLocation,
     RemoteDPDKTarballLocation,
     RemoteDPDKTreeLocation,
-    TestRunConfiguration,
 )
 from framework.exception import ConfigurationError, RemoteFileNotFoundError
+from framework.logger import DTSLogger, get_dts_logger
 from framework.params.eal import EalParams
 from framework.remote_session.remote_session import CommandResult
+from framework.testbed_model.cpu import LogicalCore, LogicalCoreCount, LogicalCoreList, lcore_filter
+from framework.testbed_model.node import Node
+from framework.testbed_model.os_session import OSSession
 from framework.testbed_model.port import Port
+from framework.testbed_model.virtual_device import VirtualDevice
 from framework.utils import MesonArgs, TarCompressionFormat
 
-from .cpu import LogicalCore, LogicalCoreList
-from .node import Node
-from .os_session import OSSession, OSSessionInfo
-from .virtual_device import VirtualDevice
-
 
 @dataclass(slots=True, frozen=True)
 class DPDKBuildInfo:
@@ -56,177 +49,36 @@ class DPDKBuildInfo:
     compiler_version: str | None
 
 
-class SutNode(Node):
-    """The system under test node.
-
-    The SUT node extends :class:`Node` with DPDK specific features:
-
-        * Managing DPDK source tree on the remote SUT,
-        * Building the DPDK from source or using a pre-built version,
-        * Gathering of DPDK build info,
-        * The running of DPDK apps, interactively or one-time execution,
-        * DPDK apps cleanup.
+class DPDKBuildEnvironment:
+    """Class handling a DPDK build environment."""
 
-    Building DPDK from source uses `build` configuration inside `dpdk_build` of configuration.
-
-    Attributes:
-        config: The SUT node configuration.
-        virtual_devices: The virtual devices used on the node.
-    """
-
-    config: SutNodeConfiguration
-    virtual_devices: list[VirtualDevice]
-    dpdk_prefix_list: list[str]
-    dpdk_timestamp: str
-    _env_vars: dict
+    config: DPDKBuildConfiguration
+    _node: Node
+    _session: OSSession
+    _logger: DTSLogger
     _remote_tmp_dir: PurePath
-    __remote_dpdk_tree_path: str | PurePath | None
+    _remote_dpdk_tree_path: str | PurePath | None
     _remote_dpdk_build_dir: PurePath | None
     _app_compile_timeout: float
-    _dpdk_kill_session: OSSession | None
-    _dpdk_version: str | None
-    _node_info: OSSessionInfo | None
-    _compiler_version: str | None
-    _path_to_devbind_script: PurePath | None
 
-    def __init__(self, node_config: SutNodeConfiguration):
-        """Extend the constructor with SUT node specifics.
+    compiler_version: str | None
 
-        Args:
-            node_config: The SUT node's test run configuration.
-        """
-        super().__init__(node_config)
-        self.lcores = self.filter_lcores(LogicalCoreList(self.config.dpdk_config.lcores))
-        if LogicalCore(lcore=0, core=0, socket=0, node=0) in self.lcores:
-            self._logger.info(
-                """
-                WARNING: First core being used;
-                using the first core is considered risky and should only
-                be done by advanced users.
-                """
-            )
-        else:
-            self._logger.info("Not using first core")
-        self.virtual_devices = []
-        self.dpdk_prefix_list = []
-        self._env_vars = {}
-        self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
-        self.__remote_dpdk_tree_path = None
+    def __init__(self, config: DPDKBuildConfiguration, node: Node):
+        """DPDK build environment class constructor."""
+        self.config = config
+        self._node = node
+        self._logger = get_dts_logger()
+        self._session = node.create_session("dpdk_build")
+
+        self._remote_tmp_dir = node.main_session.get_remote_tmp_dir()
+        self._remote_dpdk_tree_path = None
         self._remote_dpdk_build_dir = None
         self._app_compile_timeout = 90
-        self._dpdk_kill_session = None
-        self.dpdk_timestamp = (
-            f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
-        )
-        self._dpdk_version = None
-        self._node_info = None
-        self._compiler_version = None
-        self._path_to_devbind_script = None
-        self._ports_bound_to_dpdk = False
-        self._logger.info(f"Created node: {self.name}")
-
-    @property
-    def _remote_dpdk_tree_path(self) -> str | PurePath:
-        """The remote DPDK tree path."""
-        if self.__remote_dpdk_tree_path:
-            return self.__remote_dpdk_tree_path
-
-        self._logger.warning(
-            "Failed to get remote dpdk tree path because we don't know the "
-            "location on the SUT node."
-        )
-        return ""
-
-    @property
-    def remote_dpdk_build_dir(self) -> str | PurePath:
-        """The remote DPDK build dir path."""
-        if self._remote_dpdk_build_dir:
-            return self._remote_dpdk_build_dir
-
-        self._logger.warning(
-            "Failed to get remote dpdk build dir because we don't know the "
-            "location on the SUT node."
-        )
-        return ""
-
-    @property
-    def dpdk_version(self) -> str | None:
-        """Last built DPDK version."""
-        if self._dpdk_version is None:
-            self._dpdk_version = self.main_session.get_dpdk_version(self._remote_dpdk_tree_path)
-        return self._dpdk_version
-
-    @property
-    def node_info(self) -> OSSessionInfo:
-        """Additional node information."""
-        if self._node_info is None:
-            self._node_info = self.main_session.get_node_info()
-        return self._node_info
-
-    @property
-    def compiler_version(self) -> str | None:
-        """The node's compiler version."""
-        if self._compiler_version is None:
-            self._logger.warning("The `compiler_version` is None because a pre-built DPDK is used.")
 
-        return self._compiler_version
-
-    @compiler_version.setter
-    def compiler_version(self, value: str) -> None:
-        """Set the `compiler_version` used on the SUT node.
-
-        Args:
-            value: The node's compiler version.
-        """
-        self._compiler_version = value
-
-    @property
-    def path_to_devbind_script(self) -> PurePath | str:
-        """The path to the dpdk-devbind.py script on the node."""
-        if self._path_to_devbind_script is None:
-            self._path_to_devbind_script = self.main_session.join_remote_path(
-                self._remote_dpdk_tree_path, "usertools", "dpdk-devbind.py"
-            )
-        return self._path_to_devbind_script
-
-    def get_dpdk_build_info(self) -> DPDKBuildInfo:
-        """Get additional DPDK build information.
-
-        Returns:
-            The DPDK build information,
-        """
-        return DPDKBuildInfo(dpdk_version=self.dpdk_version, compiler_version=self.compiler_version)
-
-    def set_up_test_run(self, test_run_config: TestRunConfiguration, ports: Iterable[Port]) -> None:
-        """Extend the test run setup with vdev config and DPDK build set up.
-
-        This method extends the setup process by configuring virtual devices and preparing the DPDK
-        environment based on the provided configuration.
-
-        Args:
-            test_run_config: A test run configuration according to which
-                the setup steps will be taken.
-            ports: The ports to set up for the test run.
-        """
-        super().set_up_test_run(test_run_config, ports)
-        for vdev in test_run_config.vdevs:
-            self.virtual_devices.append(VirtualDevice(vdev))
-        self._set_up_dpdk(test_run_config.dpdk_config, ports)
-
-    def tear_down_test_run(self, ports: Iterable[Port]) -> None:
-        """Extend the test run teardown with virtual device teardown and DPDK teardown.
-
-        Args:
-            ports: The ports to tear down for the test run.
-        """
-        super().tear_down_test_run(ports)
-        self.virtual_devices = []
-        self._tear_down_dpdk(ports)
+        self.compiler_version = None
 
-    def _set_up_dpdk(
-        self, dpdk_build_config: DPDKBuildConfiguration, ports: Iterable[Port]
-    ) -> None:
-        """Set up DPDK the SUT node and bind ports.
+    def setup(self):
+        """Set up the DPDK build on the target node.
 
         DPDK setup includes setting all internals needed for the build, the copying of DPDK
         sources and then building DPDK or using the exist ones from the `dpdk_location`. The drivers
@@ -236,7 +88,7 @@ def _set_up_dpdk(
             dpdk_build_config: A DPDK build configuration to test.
             ports: The ports to use for DPDK.
         """
-        match dpdk_build_config.dpdk_location:
+        match self.config.dpdk_location:
             case RemoteDPDKTreeLocation(dpdk_tree=dpdk_tree):
                 self._set_remote_dpdk_tree_path(dpdk_tree)
             case LocalDPDKTreeLocation(dpdk_tree=dpdk_tree):
@@ -248,24 +100,13 @@ def _set_up_dpdk(
                 remote_tarball = self._copy_dpdk_tarball_to_remote(tarball)
                 self._prepare_and_extract_dpdk_tarball(remote_tarball)
 
-        match dpdk_build_config:
+        match self.config:
             case DPDKPrecompiledBuildConfiguration(precompiled_build_dir=build_dir):
                 self._set_remote_dpdk_build_dir(build_dir)
             case DPDKUncompiledBuildConfiguration(build_options=build_options):
                 self._configure_dpdk_build(build_options)
                 self._build_dpdk()
 
-        self.bind_ports_to_driver(ports)
-
-    def _tear_down_dpdk(self, ports: Iterable[Port]) -> None:
-        """Reset DPDK variables and bind port driver to the OS driver."""
-        self._env_vars = {}
-        self.__remote_dpdk_tree_path = None
-        self._remote_dpdk_build_dir = None
-        self._dpdk_version = None
-        self.compiler_version = None
-        self.bind_ports_to_driver(ports, for_dpdk=False)
-
     def _set_remote_dpdk_tree_path(self, dpdk_tree: PurePath):
         """Set the path to the remote DPDK source tree based on the provided DPDK location.
 
@@ -280,14 +121,14 @@ def _set_remote_dpdk_tree_path(self, dpdk_tree: PurePath):
                 is not found.
             ConfigurationError: If the remote DPDK source tree specified is not a valid directory.
         """
-        if not self.main_session.remote_path_exists(dpdk_tree):
+        if not self._session.remote_path_exists(dpdk_tree):
             raise RemoteFileNotFoundError(
                 f"Remote DPDK source tree '{dpdk_tree}' not found in SUT node."
             )
-        if not self.main_session.is_remote_dir(dpdk_tree):
+        if not self._session.is_remote_dir(dpdk_tree):
             raise ConfigurationError(f"Remote DPDK source tree '{dpdk_tree}' must be a directory.")
 
-        self.__remote_dpdk_tree_path = dpdk_tree
+        self._remote_dpdk_tree_path = dpdk_tree
 
     def _copy_dpdk_tree(self, dpdk_tree_path: Path) -> None:
         """Copy the DPDK source tree to the SUT.
@@ -298,14 +139,14 @@ def _copy_dpdk_tree(self, dpdk_tree_path: Path) -> None:
         self._logger.info(
             f"Copying DPDK source tree to SUT: '{dpdk_tree_path}' into '{self._remote_tmp_dir}'."
         )
-        self.main_session.copy_dir_to(
+        self._session.copy_dir_to(
             dpdk_tree_path,
             self._remote_tmp_dir,
             exclude=[".git", "*.o"],
             compress_format=TarCompressionFormat.gzip,
         )
 
-        self.__remote_dpdk_tree_path = self.main_session.join_remote_path(
+        self._remote_dpdk_tree_path = self._session.join_remote_path(
             self._remote_tmp_dir, PurePath(dpdk_tree_path).name
         )
 
@@ -320,9 +161,9 @@ def _validate_remote_dpdk_tarball(self, dpdk_tarball: PurePath) -> None:
                 not found.
             ConfigurationError: If the `dpdk_tarball` is a valid path but not a valid tar archive.
         """
-        if not self.main_session.remote_path_exists(dpdk_tarball):
+        if not self._session.remote_path_exists(dpdk_tarball):
             raise RemoteFileNotFoundError(f"Remote DPDK tarball '{dpdk_tarball}' not found in SUT.")
-        if not self.main_session.is_remote_tarfile(dpdk_tarball):
+        if not self._session.is_remote_tarfile(dpdk_tarball):
             raise ConfigurationError(f"Remote DPDK tarball '{dpdk_tarball}' must be a tar archive.")
 
     def _copy_dpdk_tarball_to_remote(self, dpdk_tarball: Path) -> PurePath:
@@ -337,8 +178,8 @@ def _copy_dpdk_tarball_to_remote(self, dpdk_tarball: Path) -> PurePath:
         self._logger.info(
             f"Copying DPDK tarball to SUT: '{dpdk_tarball}' into '{self._remote_tmp_dir}'."
         )
-        self.main_session.copy_to(dpdk_tarball, self._remote_tmp_dir)
-        return self.main_session.join_remote_path(self._remote_tmp_dir, dpdk_tarball.name)
+        self._session.copy_to(dpdk_tarball, self._remote_tmp_dir)
+        return self._session.join_remote_path(self._remote_tmp_dir, dpdk_tarball.name)
 
     def _prepare_and_extract_dpdk_tarball(self, remote_tarball_path: PurePath) -> None:
         """Prepare the remote DPDK tree path and extract the tarball.
@@ -365,19 +206,19 @@ def remove_tarball_suffix(remote_tarball_path: PurePath) -> PurePath:
                     return PurePath(str(remote_tarball_path).replace(suffixes_to_remove, ""))
             return remote_tarball_path.with_suffix("")
 
-        tarball_top_dir = self.main_session.get_tarball_top_dir(remote_tarball_path)
-        self.__remote_dpdk_tree_path = self.main_session.join_remote_path(
+        tarball_top_dir = self._session.get_tarball_top_dir(remote_tarball_path)
+        self._remote_dpdk_tree_path = self._session.join_remote_path(
             remote_tarball_path.parent,
             tarball_top_dir or remove_tarball_suffix(remote_tarball_path),
         )
 
         self._logger.info(
             "Extracting DPDK tarball on SUT: "
-            f"'{remote_tarball_path}' into '{self._remote_dpdk_tree_path}'."
+            f"'{remote_tarball_path}' into '{self.remote_dpdk_tree_path}'."
         )
-        self.main_session.extract_remote_tarball(
+        self._session.extract_remote_tarball(
             remote_tarball_path,
-            self._remote_dpdk_tree_path,
+            self.remote_dpdk_tree_path,
         )
 
     def _set_remote_dpdk_build_dir(self, build_dir: str):
@@ -395,10 +236,10 @@ def _set_remote_dpdk_build_dir(self, build_dir: str):
             RemoteFileNotFoundError: If the `build_dir` is expected but does not exist on the SUT
                 node.
         """
-        remote_dpdk_build_dir = self.main_session.join_remote_path(
-            self._remote_dpdk_tree_path, build_dir
+        remote_dpdk_build_dir = self._session.join_remote_path(
+            self.remote_dpdk_tree_path, build_dir
         )
-        if not self.main_session.remote_path_exists(remote_dpdk_build_dir):
+        if not self._session.remote_path_exists(remote_dpdk_build_dir):
             raise RemoteFileNotFoundError(
                 f"Remote DPDK build dir '{remote_dpdk_build_dir}' not found in SUT node."
             )
@@ -415,20 +256,18 @@ def _configure_dpdk_build(self, dpdk_build_config: DPDKBuildOptionsConfiguration
             dpdk_build_config: A DPDK build configuration to test.
         """
         self._env_vars = {}
-        self._env_vars.update(self.main_session.get_dpdk_build_env_vars(self.arch))
+        self._env_vars.update(self._session.get_dpdk_build_env_vars(self._node.arch))
         if compiler_wrapper := dpdk_build_config.compiler_wrapper:
             self._env_vars["CC"] = f"'{compiler_wrapper} {dpdk_build_config.compiler.name}'"
         else:
             self._env_vars["CC"] = dpdk_build_config.compiler.name
 
-        self.compiler_version = self.main_session.get_compiler_version(
-            dpdk_build_config.compiler.name
-        )
+        self.compiler_version = self._session.get_compiler_version(dpdk_build_config.compiler.name)
 
-        build_dir_name = f"{self.arch}-{self.config.os}-{dpdk_build_config.compiler}"
+        build_dir_name = f"{self._node.arch}-{self._node.config.os}-{dpdk_build_config.compiler}"
 
-        self._remote_dpdk_build_dir = self.main_session.join_remote_path(
-            self._remote_dpdk_tree_path, build_dir_name
+        self._remote_dpdk_build_dir = self._session.join_remote_path(
+            self.remote_dpdk_tree_path, build_dir_name
         )
 
     def _build_dpdk(self) -> None:
@@ -437,10 +276,10 @@ def _build_dpdk(self) -> None:
         Uses the already configured DPDK build configuration. Assumes that the
         `_remote_dpdk_tree_path` has already been set on the SUT node.
         """
-        self.main_session.build_dpdk(
+        self._session.build_dpdk(
             self._env_vars,
             MesonArgs(default_library="static", enable_kmods=True, libdir="lib"),
-            self._remote_dpdk_tree_path,
+            self.remote_dpdk_tree_path,
             self.remote_dpdk_build_dir,
         )
 
@@ -459,31 +298,120 @@ def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePa
             The directory path of the built app. If building all apps, return
             the path to the examples directory (where all apps reside).
         """
-        self.main_session.build_dpdk(
+        self._session.build_dpdk(
             self._env_vars,
             MesonArgs(examples=app_name, **meson_dpdk_args),  # type: ignore [arg-type]
             # ^^ https://github.com/python/mypy/issues/11583
-            self._remote_dpdk_tree_path,
+            self.remote_dpdk_tree_path,
             self.remote_dpdk_build_dir,
             rebuild=True,
             timeout=self._app_compile_timeout,
         )
 
         if app_name == "all":
-            return self.main_session.join_remote_path(self.remote_dpdk_build_dir, "examples")
-        return self.main_session.join_remote_path(
+            return self._session.join_remote_path(self.remote_dpdk_build_dir, "examples")
+        return self._session.join_remote_path(
             self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}"
         )
 
-    def kill_cleanup_dpdk_apps(self) -> None:
-        """Kill all dpdk applications on the SUT, then clean up hugepages."""
-        if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
-            # we can use the session if it exists and responds
-            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list)
+    @property
+    def remote_dpdk_tree_path(self) -> str | PurePath:
+        """The remote DPDK tree path."""
+        if self._remote_dpdk_tree_path:
+            return self._remote_dpdk_tree_path
+
+        self._logger.warning(
+            "Failed to get remote dpdk tree path because we don't know the "
+            "location on the SUT node."
+        )
+        return ""
+
+    @property
+    def remote_dpdk_build_dir(self) -> str | PurePath:
+        """The remote DPDK build dir path."""
+        if self._remote_dpdk_build_dir:
+            return self._remote_dpdk_build_dir
+
+        self._logger.warning(
+            "Failed to get remote dpdk build dir because we don't know the "
+            "location on the SUT node."
+        )
+        return ""
+
+    @cached_property
+    def dpdk_version(self) -> str | None:
+        """Last built DPDK version."""
+        return self._session.get_dpdk_version(self.remote_dpdk_tree_path)
+
+    def get_dpdk_build_info(self) -> DPDKBuildInfo:
+        """Get additional DPDK build information.
+
+        Returns:
+            The DPDK build information,
+        """
+        return DPDKBuildInfo(dpdk_version=self.dpdk_version, compiler_version=self.compiler_version)
+
+
+class DPDKRuntimeEnvironment:
+    """Class handling a DPDK runtime environment."""
+
+    config: Final[DPDKRuntimeConfiguration]
+    build: Final[DPDKBuildEnvironment]
+    _node: Final[Node]
+    _logger: Final[DTSLogger]
+
+    timestamp: Final[str]
+    _virtual_devices: list[VirtualDevice]
+    _lcores: list[LogicalCore]
+
+    _env_vars: dict
+    _kill_session: OSSession | None
+    prefix_list: list[str]
+
+    def __init__(
+        self,
+        config: DPDKRuntimeConfiguration,
+        node: Node,
+        build_env: DPDKBuildEnvironment,
+    ):
+        """DPDK environment constructor.
+
+        Args:
+            config: The configuration of DPDK.
+            node: The target node to manage a DPDK environment.
+            build_env: The DPDK build environment.
+        """
+        self.config = config
+        self.build = build_env
+        self._node = node
+        self._logger = get_dts_logger()
+
+        self.timestamp = f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
+        self._virtual_devices = [VirtualDevice(vdev) for vdev in config.vdevs]
+
+        self._lcores = node.lcores
+        self._lcores = self.filter_lcores(LogicalCoreList(self.config.lcores))
+        if LogicalCore(lcore=0, core=0, socket=0, node=0) in self._lcores:
+            self._logger.warning(
+                "First core being used; "
+                "the first core is considered risky and should only be done by advanced users."
+            )
         else:
-            # otherwise, we need to (re)create it
-            self._dpdk_kill_session = self.create_session("dpdk_kill")
-        self.dpdk_prefix_list = []
+            self._logger.info("Not using first core")
+
+        self.prefix_list = []
+        self._env_vars = {}
+        self._ports_bound_to_dpdk = False
+        self._kill_session = None
+
+    def setup(self, ports: Iterable[Port]):
+        """Set up the DPDK runtime on the target node."""
+        self.build.setup()
+        self.bind_ports_to_driver(ports)
+
+    def teardown(self, ports: Iterable[Port]) -> None:
+        """Reset DPDK variables and bind port driver to the OS driver."""
+        self.bind_ports_to_driver(ports, for_dpdk=False)
 
     def run_dpdk_app(
         self, app_path: PurePath, eal_params: EalParams, timeout: float = 30
@@ -501,7 +429,7 @@ def run_dpdk_app(
         Returns:
             The result of the DPDK app execution.
         """
-        return self.main_session.send_command(
+        return self._node.main_session.send_command(
             f"{app_path} {eal_params}", timeout, privileged=True, verify=True
         )
 
@@ -518,9 +446,59 @@ def bind_ports_to_driver(self, ports: Iterable[Port], for_dpdk: bool = True) ->
                 continue
 
             driver = port.config.os_driver_for_dpdk if for_dpdk else port.config.os_driver
-            self.main_session.send_command(
-                f"{self.path_to_devbind_script} -b {driver} --force {port.pci}",
+            self._node.main_session.send_command(
+                f"{self.devbind_script_path} -b {driver} --force {port.pci}",
                 privileged=True,
                 verify=True,
             )
             port.bound_for_dpdk = for_dpdk
+
+    @cached_property
+    def devbind_script_path(self) -> PurePath:
+        """The path to the dpdk-devbind.py script on the node."""
+        return self._node.main_session.join_remote_path(
+            self.build.remote_dpdk_tree_path, "usertools", "dpdk-devbind.py"
+        )
+
+    def filter_lcores(
+        self,
+        filter_specifier: LogicalCoreCount | LogicalCoreList,
+        ascending: bool = True,
+    ) -> list[LogicalCore]:
+        """Filter the node's logical cores that DTS can use.
+
+        Logical cores that DTS can use are the ones that are present on the node, but filtered
+        according to the test run configuration. The `filter_specifier` will filter cores from
+        those logical cores.
+
+        Args:
+            filter_specifier: Two different filters can be used, one that specifies the number
+                of logical cores per core, cores per socket and the number of sockets,
+                and another one that specifies a logical core list.
+            ascending: If :data:`True`, use cores with the lowest numerical id first and continue
+                in ascending order. If :data:`False`, start with the highest id and continue
+                in descending order. This ordering affects which sockets to consider first as well.
+
+        Returns:
+            The filtered logical cores.
+        """
+        self._logger.debug(f"Filtering {filter_specifier} from {self._lcores}.")
+        return lcore_filter(
+            self._lcores,
+            filter_specifier,
+            ascending,
+        ).filter()
+
+    def kill_cleanup_dpdk_apps(self) -> None:
+        """Kill all dpdk applications on the SUT, then clean up hugepages."""
+        if self._kill_session and self._kill_session.is_alive():
+            # we can use the session if it exists and responds
+            self._kill_session.kill_cleanup_dpdk_apps(self.prefix_list)
+        else:
+            # otherwise, we need to (re)create it
+            self._kill_session = self._node.create_session("dpdk_kill")
+        self.prefix_list = []
+
+    def get_virtual_devices(self) -> Iterable[VirtualDevice]:
+        """The available DPDK virtual devices."""
+        return (v for v in self._virtual_devices)
diff --git a/dts/framework/remote_session/dpdk_shell.py b/dts/framework/remote_session/dpdk_shell.py
index b55deb7fa0..fc43448e06 100644
--- a/dts/framework/remote_session/dpdk_shell.py
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -15,7 +15,6 @@
     SingleActiveInteractiveShell,
 )
 from framework.testbed_model.cpu import LogicalCoreList
-from framework.testbed_model.sut_node import SutNode
 
 
 def compute_eal_params(
@@ -35,15 +34,15 @@ def compute_eal_params(
 
     if params.lcore_list is None:
         params.lcore_list = LogicalCoreList(
-            ctx.sut_node.filter_lcores(ctx.local.lcore_filter_specifier, ctx.local.ascending_cores)
+            ctx.dpdk.filter_lcores(ctx.local.lcore_filter_specifier, ctx.local.ascending_cores)
         )
 
     prefix = params.prefix
     if ctx.local.append_prefix_timestamp:
-        prefix = f"{prefix}_{ctx.sut_node.dpdk_timestamp}"
+        prefix = f"{prefix}_{ctx.dpdk.timestamp}"
     prefix = ctx.sut_node.main_session.get_dpdk_file_prefix(prefix)
     if prefix:
-        ctx.sut_node.dpdk_prefix_list.append(prefix)
+        ctx.dpdk.prefix_list.append(prefix)
     params.prefix = prefix
 
     if params.allowed_ports is None:
@@ -60,7 +59,6 @@ class DPDKShell(SingleActiveInteractiveShell, ABC):
     supplied app parameters.
     """
 
-    _node: SutNode
     _app_params: EalParams
 
     def __init__(
@@ -80,4 +78,6 @@ def _update_real_path(self, path: PurePath) -> None:
 
         Adds the remote DPDK build directory to the path.
         """
-        super()._update_real_path(PurePath(self._node.remote_dpdk_build_dir).joinpath(path))
+        super()._update_real_path(
+            PurePath(get_ctx().dpdk.build.remote_dpdk_build_dir).joinpath(path)
+        )
diff --git a/dts/framework/remote_session/single_active_interactive_shell.py b/dts/framework/remote_session/single_active_interactive_shell.py
index 2eec2f698a..c1369ef77e 100644
--- a/dts/framework/remote_session/single_active_interactive_shell.py
+++ b/dts/framework/remote_session/single_active_interactive_shell.py
@@ -27,7 +27,6 @@
 from paramiko import Channel, channel
 from typing_extensions import Self
 
-from framework.context import get_ctx
 from framework.exception import (
     InteractiveCommandExecutionError,
     InteractiveSSHSessionDeadError,
@@ -35,6 +34,7 @@
 )
 from framework.logger import DTSLogger, get_dts_logger
 from framework.params import Params
+from framework.settings import SETTINGS
 from framework.testbed_model.node import Node
 from framework.utils import MultiInheritanceBaseClass
 
@@ -114,7 +114,7 @@ def __init__(
         self._logger = get_dts_logger(f"{node.name}.{name}")
         self._app_params = app_params
         self._privileged = privileged
-        self._timeout = get_ctx().local.timeout
+        self._timeout = SETTINGS.timeout
         # Ensure path is properly formatted for the host
         self._update_real_path(self.path)
         super().__init__(**kwargs)
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index 90aeb63cfb..801709a2aa 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -15,17 +15,11 @@
 from framework.config.common import ValidationContext
 from framework.test_run import TestRun
 from framework.testbed_model.node import Node
-from framework.testbed_model.sut_node import SutNode
-from framework.testbed_model.tg_node import TGNode
 
 from .config import (
     Configuration,
     load_config,
 )
-from .config.node import (
-    SutNodeConfiguration,
-    TGNodeConfiguration,
-)
 from .logger import DTSLogger, get_dts_logger
 from .settings import SETTINGS
 from .test_result import (
@@ -63,15 +57,7 @@ def run(self) -> None:
             self._result.update_setup(Result.PASS)
 
             for node_config in self._configuration.nodes:
-                node: Node
-
-                match node_config:
-                    case SutNodeConfiguration():
-                        node = SutNode(node_config)
-                    case TGNodeConfiguration():
-                        node = TGNode(node_config)
-
-                nodes.append(node)
+                nodes.append(Node(node_config))
 
             # for all test run sections
             for test_run_config in self._configuration.test_runs:
diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py
index a59bac71bb..7f576022c7 100644
--- a/dts/framework/test_result.py
+++ b/dts/framework/test_result.py
@@ -32,9 +32,9 @@
 from .config.test_run import TestRunConfiguration
 from .exception import DTSError, ErrorSeverity
 from .logger import DTSLogger
+from .remote_session.dpdk import DPDKBuildInfo
 from .testbed_model.os_session import OSSessionInfo
 from .testbed_model.port import Port
-from .testbed_model.sut_node import DPDKBuildInfo
 
 
 class Result(Enum):
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index 811798f57f..6801bf87fd 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -80,7 +80,7 @@
 from functools import cached_property
 from pathlib import Path
 from types import MethodType
-from typing import ClassVar, Protocol, Union, cast
+from typing import ClassVar, Protocol, Union
 
 from framework.config.test_run import TestRunConfiguration
 from framework.context import Context, init_ctx
@@ -90,6 +90,7 @@
     TestCaseVerifyError,
 )
 from framework.logger import DTSLogger, get_dts_logger
+from framework.remote_session.dpdk import DPDKBuildEnvironment, DPDKRuntimeEnvironment
 from framework.settings import SETTINGS
 from framework.test_result import BaseResult, Result, TestCaseResult, TestRunResult, TestSuiteResult
 from framework.test_suite import TestCase, TestSuite
@@ -99,9 +100,8 @@
     test_if_supported,
 )
 from framework.testbed_model.node import Node
-from framework.testbed_model.sut_node import SutNode
-from framework.testbed_model.tg_node import TGNode
 from framework.testbed_model.topology import PortLink, Topology
+from framework.testbed_model.traffic_generator import create_traffic_generator
 
 TestScenario = tuple[type[TestSuite], deque[type[TestCase]]]
 
@@ -163,17 +163,18 @@ def __init__(self, config: TestRunConfiguration, nodes: Iterable[Node], result:
         self.logger = get_dts_logger()
 
         sut_node = next(n for n in nodes if n.name == config.system_under_test_node)
-        sut_node = cast(SutNode, sut_node)  # Config validation must render this valid.
-
         tg_node = next(n for n in nodes if n.name == config.traffic_generator_node)
-        tg_node = cast(TGNode, tg_node)  # Config validation must render this valid.
 
         topology = Topology.from_port_links(
             PortLink(sut_node.ports_by_name[link.sut_port], tg_node.ports_by_name[link.tg_port])
             for link in self.config.port_topology
         )
 
-        self.ctx = Context(sut_node, tg_node, topology)
+        dpdk_build_env = DPDKBuildEnvironment(config.dpdk.build, sut_node)
+        dpdk_runtime_env = DPDKRuntimeEnvironment(config.dpdk, sut_node, dpdk_build_env)
+        traffic_generator = create_traffic_generator(config.traffic_generator, tg_node)
+
+        self.ctx = Context(sut_node, tg_node, topology, dpdk_runtime_env, traffic_generator)
         self.result = result
         self.selected_tests = list(self.config.filter_tests())
         self.blocked = False
@@ -307,11 +308,11 @@ def next(self) -> State | None:
         test_run.init_random_seed()
         test_run.remaining_tests = deque(test_run.selected_tests)
 
-        test_run.ctx.sut_node.set_up_test_run(test_run.config, test_run.ctx.topology.sut_ports)
+        test_run.ctx.dpdk.setup(test_run.ctx.topology.sut_ports)
 
         self.result.ports = test_run.ctx.topology.sut_ports + test_run.ctx.topology.tg_ports
         self.result.sut_info = test_run.ctx.sut_node.node_info
-        self.result.dpdk_build_info = test_run.ctx.sut_node.get_dpdk_build_info()
+        self.result.dpdk_build_info = test_run.ctx.dpdk.build.get_dpdk_build_info()
 
         self.logger.debug(f"Found capabilities to check: {test_run.required_capabilities}")
         test_run.supported_capabilities = get_supported_capabilities(
@@ -390,7 +391,7 @@ def description(self) -> str:
 
     def next(self) -> State | None:
         """Next state."""
-        self.test_run.ctx.sut_node.tear_down_test_run(self.test_run.ctx.topology.sut_ports)
+        self.test_run.ctx.dpdk.teardown(self.test_run.ctx.topology.sut_ports)
         self.result.update_teardown(Result.PASS)
         return None
 
@@ -500,7 +501,7 @@ def description(self) -> str:
     def next(self) -> State | None:
         """Next state."""
         self.test_suite.tear_down_suite()
-        self.test_run.ctx.sut_node.kill_cleanup_dpdk_apps()
+        self.test_run.ctx.dpdk.kill_cleanup_dpdk_apps()
         self.result.update_teardown(Result.PASS)
         return TestRunExecution(self.test_run, self.test_run.result)
 
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index ae90997061..58da26adf0 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -34,6 +34,7 @@
 from framework.testbed_model.capability import TestProtocol
 from framework.testbed_model.topology import Topology
 from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+    CapturingTrafficGenerator,
     PacketFilteringConfig,
 )
 
@@ -246,8 +247,12 @@ def send_packets_and_capture(
         Returns:
             A list of received packets.
         """
+        assert isinstance(
+            self._ctx.tg, CapturingTrafficGenerator
+        ), "Cannot capture with a non-capturing traffic generator"
+        # TODO: implement @requires for types of traffic generator
         packets = self._adjust_addresses(packets)
-        return self._ctx.tg_node.send_packets_and_capture(
+        return self._ctx.tg.send_packets_and_capture(
             packets,
             self._ctx.topology.tg_port_egress,
             self._ctx.topology.tg_port_ingress,
@@ -265,7 +270,7 @@ def send_packets(
             packets: Packets to send.
         """
         packets = self._adjust_addresses(packets)
-        self._ctx.tg_node.send_packets(packets, self._ctx.topology.tg_port_egress)
+        self._ctx.tg.send_packets(packets, self._ctx.topology.tg_port_egress)
 
     def get_expected_packets(
         self,
diff --git a/dts/framework/testbed_model/capability.py b/dts/framework/testbed_model/capability.py
index a1d6d9dd32..ea0e647a47 100644
--- a/dts/framework/testbed_model/capability.py
+++ b/dts/framework/testbed_model/capability.py
@@ -63,8 +63,8 @@ def test_scatter_mbuf_2048(self):
     TestPmdShellDecorator,
     TestPmdShellMethod,
 )
+from framework.testbed_model.node import Node
 
-from .sut_node import SutNode
 from .topology import Topology, TopologyType
 
 if TYPE_CHECKING:
@@ -90,7 +90,7 @@ class Capability(ABC):
     #: A set storing the capabilities whose support should be checked.
     capabilities_to_check: ClassVar[set[Self]] = set()
 
-    def register_to_check(self) -> Callable[[SutNode, "Topology"], set[Self]]:
+    def register_to_check(self) -> Callable[[Node, "Topology"], set[Self]]:
         """Register the capability to be checked for support.
 
         Returns:
@@ -118,27 +118,27 @@ def _preprocess_required(self, test_case_or_suite: type["TestProtocol"]) -> None
         """An optional method that modifies the required capabilities."""
 
     @classmethod
-    def _get_and_reset(cls, sut_node: SutNode, topology: "Topology") -> set[Self]:
+    def _get_and_reset(cls, node: Node, topology: "Topology") -> set[Self]:
         """The callback method to be called after all capabilities have been registered.
 
         Not only does this method check the support of capabilities,
         but it also reset the internal set of registered capabilities
         so that the "register, then get support" workflow works in subsequent test runs.
         """
-        supported_capabilities = cls.get_supported_capabilities(sut_node, topology)
+        supported_capabilities = cls.get_supported_capabilities(node, topology)
         cls.capabilities_to_check = set()
         return supported_capabilities
 
     @classmethod
     @abstractmethod
-    def get_supported_capabilities(cls, sut_node: SutNode, topology: "Topology") -> set[Self]:
+    def get_supported_capabilities(cls, node: Node, topology: "Topology") -> set[Self]:
         """Get the support status of each registered capability.
 
         Each subclass must implement this method and return the subset of supported capabilities
         of :attr:`capabilities_to_check`.
 
         Args:
-            sut_node: The SUT node of the current test run.
+            node: The node to check capabilities against.
             topology: The topology of the current test run.
 
         Returns:
@@ -197,7 +197,7 @@ def get_unique(cls, nic_capability: NicCapability) -> Self:
 
     @classmethod
     def get_supported_capabilities(
-        cls, sut_node: SutNode, topology: "Topology"
+        cls, node: Node, topology: "Topology"
     ) -> set["DecoratedNicCapability"]:
         """Overrides :meth:`~Capability.get_supported_capabilities`.
 
@@ -207,7 +207,7 @@ def get_supported_capabilities(
         before executing its `capability_fn` so that each capability is retrieved only once.
         """
         supported_conditional_capabilities: set["DecoratedNicCapability"] = set()
-        logger = get_dts_logger(f"{sut_node.name}.{cls.__name__}")
+        logger = get_dts_logger(f"{node.name}.{cls.__name__}")
         if topology.type is topology.type.no_link:
             logger.debug(
                 "No links available in the current topology, not getting NIC capabilities."
@@ -332,7 +332,7 @@ def get_unique(cls, topology_type: TopologyType) -> Self:
 
     @classmethod
     def get_supported_capabilities(
-        cls, sut_node: SutNode, topology: "Topology"
+        cls, node: Node, topology: "Topology"
     ) -> set["TopologyCapability"]:
         """Overrides :meth:`~Capability.get_supported_capabilities`."""
         supported_capabilities = set()
@@ -483,14 +483,14 @@ def add_required_capability(
 
 
 def get_supported_capabilities(
-    sut_node: SutNode,
+    node: Node,
     topology_config: Topology,
     capabilities_to_check: set[Capability],
 ) -> set[Capability]:
     """Probe the environment for `capabilities_to_check` and return the supported ones.
 
     Args:
-        sut_node: The SUT node to check for capabilities.
+        node: The node to check capabilities against.
         topology_config: The topology config to check for capabilities.
         capabilities_to_check: The capabilities to check.
 
@@ -502,7 +502,7 @@ def get_supported_capabilities(
         callbacks.add(capability_to_check.register_to_check())
     supported_capabilities = set()
     for callback in callbacks:
-        supported_capabilities.update(callback(sut_node, topology_config))
+        supported_capabilities.update(callback(node, topology_config))
 
     return supported_capabilities
 
diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py
index 1a4c825ed2..be1b4ac2ac 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -13,25 +13,22 @@
 The :func:`~Node.skip_setup` decorator can be used without subclassing.
 """
 
-from abc import ABC
-from collections.abc import Iterable
 from functools import cached_property
 
 from framework.config.node import (
     OS,
     NodeConfiguration,
 )
-from framework.config.test_run import TestRunConfiguration
 from framework.exception import ConfigurationError
 from framework.logger import DTSLogger, get_dts_logger
 
-from .cpu import Architecture, LogicalCore, LogicalCoreCount, LogicalCoreList, lcore_filter
+from .cpu import Architecture, LogicalCore
 from .linux_session import LinuxSession
-from .os_session import OSSession
+from .os_session import OSSession, OSSessionInfo
 from .port import Port
 
 
-class Node(ABC):
+class Node:
     """The base class for node management.
 
     It shouldn't be instantiated, but rather subclassed.
@@ -57,7 +54,8 @@ class Node(ABC):
     ports: list[Port]
     _logger: DTSLogger
     _other_sessions: list[OSSession]
-    _test_run_config: TestRunConfiguration
+    _node_info: OSSessionInfo | None
+    _compiler_version: str | None
 
     def __init__(self, node_config: NodeConfiguration):
         """Connect to the node and gather info during initialization.
@@ -80,35 +78,13 @@ def __init__(self, node_config: NodeConfiguration):
         self._get_remote_cpus()
         self._other_sessions = []
         self.ports = [Port(self, port_config) for port_config in self.config.ports]
+        self._logger.info(f"Created node: {self.name}")
 
     @cached_property
     def ports_by_name(self) -> dict[str, Port]:
         """Ports mapped by the name assigned at configuration."""
         return {port.name: port for port in self.ports}
 
-    def set_up_test_run(self, test_run_config: TestRunConfiguration, ports: Iterable[Port]) -> None:
-        """Test run setup steps.
-
-        Configure hugepages on all DTS node types. Additional steps can be added by
-        extending the method in subclasses with the use of super().
-
-        Args:
-            test_run_config: A test run configuration according to which
-                the setup steps will be taken.
-            ports: The ports to set up for the test run.
-        """
-        self._setup_hugepages()
-
-    def tear_down_test_run(self, ports: Iterable[Port]) -> None:
-        """Test run teardown steps.
-
-        There are currently no common execution teardown steps common to all DTS node types.
-        Additional steps can be added by extending the method in subclasses with the use of super().
-
-        Args:
-            ports: The ports to tear down for the test run.
-        """
-
     def create_session(self, name: str) -> OSSession:
         """Create and return a new OS-aware remote session.
 
@@ -134,40 +110,33 @@ def create_session(self, name: str) -> OSSession:
         self._other_sessions.append(connection)
         return connection
 
-    def filter_lcores(
-        self,
-        filter_specifier: LogicalCoreCount | LogicalCoreList,
-        ascending: bool = True,
-    ) -> list[LogicalCore]:
-        """Filter the node's logical cores that DTS can use.
-
-        Logical cores that DTS can use are the ones that are present on the node, but filtered
-        according to the test run configuration. The `filter_specifier` will filter cores from
-        those logical cores.
-
-        Args:
-            filter_specifier: Two different filters can be used, one that specifies the number
-                of logical cores per core, cores per socket and the number of sockets,
-                and another one that specifies a logical core list.
-            ascending: If :data:`True`, use cores with the lowest numerical id first and continue
-                in ascending order. If :data:`False`, start with the highest id and continue
-                in descending order. This ordering affects which sockets to consider first as well.
-
-        Returns:
-            The filtered logical cores.
-        """
-        self._logger.debug(f"Filtering {filter_specifier} from {self.lcores}.")
-        return lcore_filter(
-            self.lcores,
-            filter_specifier,
-            ascending,
-        ).filter()
-
     def _get_remote_cpus(self) -> None:
         """Scan CPUs in the remote OS and store a list of LogicalCores."""
         self._logger.info("Getting CPU information.")
         self.lcores = self.main_session.get_remote_cpus()
 
+    @cached_property
+    def node_info(self) -> OSSessionInfo:
+        """Additional node information."""
+        return self.main_session.get_node_info()
+
+    @property
+    def compiler_version(self) -> str | None:
+        """The node's compiler version."""
+        if self._compiler_version is None:
+            self._logger.warning("The `compiler_version` is None because a pre-built DPDK is used.")
+
+        return self._compiler_version
+
+    @compiler_version.setter
+    def compiler_version(self, value: str) -> None:
+        """Set the `compiler_version` used on the SUT node.
+
+        Args:
+            value: The node's compiler version.
+        """
+        self._compiler_version = value
+
     def _setup_hugepages(self) -> None:
         """Setup hugepages on the node.
 
diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py
deleted file mode 100644
index 290a3fbd74..0000000000
--- a/dts/framework/testbed_model/tg_node.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# SPDX-License-Identifier: BSD-3-Clause
-# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022 University of New Hampshire
-# Copyright(c) 2023 PANTHEON.tech s.r.o.
-
-"""Traffic generator node.
-
-A traffic generator (TG) generates traffic that's sent towards the SUT node.
-A TG node is where the TG runs.
-"""
-
-from collections.abc import Iterable
-
-from scapy.packet import Packet
-
-from framework.config.node import TGNodeConfiguration
-from framework.config.test_run import TestRunConfiguration
-from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
-    PacketFilteringConfig,
-)
-
-from .node import Node
-from .port import Port
-from .traffic_generator import CapturingTrafficGenerator, create_traffic_generator
-
-
-class TGNode(Node):
-    """The traffic generator node.
-
-    The TG node extends :class:`Node` with TG specific features:
-
-        * Traffic generator initialization,
-        * The sending of traffic and receiving packets,
-        * The sending of traffic without receiving packets.
-
-    Not all traffic generators are capable of capturing traffic, which is why there
-    must be a way to send traffic without that.
-
-    Attributes:
-        config: The traffic generator node configuration.
-        traffic_generator: The traffic generator running on the node.
-    """
-
-    config: TGNodeConfiguration
-    traffic_generator: CapturingTrafficGenerator
-
-    def __init__(self, node_config: TGNodeConfiguration):
-        """Extend the constructor with TG node specifics.
-
-        Initialize the traffic generator on the TG node.
-
-        Args:
-            node_config: The TG node's test run configuration.
-        """
-        super().__init__(node_config)
-        self._logger.info(f"Created node: {self.name}")
-
-    def set_up_test_run(self, test_run_config: TestRunConfiguration, ports: Iterable[Port]) -> None:
-        """Extend the test run setup with the setup of the traffic generator.
-
-        Args:
-            test_run_config: A test run configuration according to which
-                the setup steps will be taken.
-            ports: The ports to set up for the test run.
-        """
-        super().set_up_test_run(test_run_config, ports)
-        self.main_session.bring_up_link(ports)
-        self.traffic_generator = create_traffic_generator(self, self.config.traffic_generator)
-
-    def tear_down_test_run(self, ports: Iterable[Port]) -> None:
-        """Extend the test run teardown with the teardown of the traffic generator.
-
-        Args:
-            ports: The ports to tear down for the test run.
-        """
-        super().tear_down_test_run(ports)
-        self.traffic_generator.close()
-
-    def send_packets_and_capture(
-        self,
-        packets: list[Packet],
-        send_port: Port,
-        receive_port: Port,
-        filter_config: PacketFilteringConfig = PacketFilteringConfig(),
-        duration: float = 1,
-    ) -> list[Packet]:
-        """Send `packets`, return received traffic.
-
-        Send `packets` on `send_port` and then return all traffic captured
-        on `receive_port` for the given duration. Also record the captured traffic
-        in a pcap file.
-
-        Args:
-            packets: The packets to send.
-            send_port: The egress port on the TG node.
-            receive_port: The ingress port in the TG node.
-            filter_config: The filter to use when capturing packets.
-            duration: Capture traffic for this amount of time after sending `packet`.
-
-        Returns:
-             A list of received packets. May be empty if no packets are captured.
-        """
-        return self.traffic_generator.send_packets_and_capture(
-            packets,
-            send_port,
-            receive_port,
-            filter_config,
-            duration,
-        )
-
-    def send_packets(self, packets: list[Packet], port: Port):
-        """Send packets without capturing resulting received packets.
-
-        Args:
-            packets: Packets to send.
-            port: Port to send the packets on.
-        """
-        self.traffic_generator.send_packets(packets, port)
-
-    def close(self) -> None:
-        """Free all resources used by the node.
-
-        This extends the superclass method with TG cleanup.
-        """
-        super().close()
diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py
index 922875f401..2a259a6e6c 100644
--- a/dts/framework/testbed_model/traffic_generator/__init__.py
+++ b/dts/framework/testbed_model/traffic_generator/__init__.py
@@ -14,7 +14,7 @@
 and a capturing traffic generator is required.
 """
 
-from framework.config.node import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig
+from framework.config.test_run import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig
 from framework.exception import ConfigurationError
 from framework.testbed_model.node import Node
 
@@ -23,13 +23,13 @@
 
 
 def create_traffic_generator(
-    tg_node: Node, traffic_generator_config: TrafficGeneratorConfig
+    traffic_generator_config: TrafficGeneratorConfig, node: Node
 ) -> CapturingTrafficGenerator:
     """The factory function for creating traffic generator objects from the test run configuration.
 
     Args:
-        tg_node: The traffic generator node where the created traffic generator will be running.
         traffic_generator_config: The traffic generator config.
+        node: The node where the created traffic generator will be running.
 
     Returns:
         A traffic generator capable of capturing received packets.
@@ -39,6 +39,6 @@ def create_traffic_generator(
     """
     match traffic_generator_config:
         case ScapyTrafficGeneratorConfig():
-            return ScapyTrafficGenerator(tg_node, traffic_generator_config, privileged=True)
+            return ScapyTrafficGenerator(node, traffic_generator_config, privileged=True)
         case _:
             raise ConfigurationError(f"Unknown traffic generator: {traffic_generator_config.type}")
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index c9c7dac54a..520561b2eb 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -14,13 +14,15 @@
 
 import re
 import time
+from collections.abc import Iterable
 from typing import ClassVar
 
 from scapy.compat import base64_bytes
 from scapy.layers.l2 import Ether
 from scapy.packet import Packet
 
-from framework.config.node import OS, ScapyTrafficGeneratorConfig
+from framework.config.node import OS
+from framework.config.test_run import ScapyTrafficGeneratorConfig
 from framework.remote_session.python_shell import PythonShell
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
@@ -83,6 +85,14 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig, **kwargs)
         super().__init__(node=tg_node, config=config, tg_node=tg_node, **kwargs)
         self.start_application()
 
+    def setup(self, ports: Iterable[Port]):
+        """Extends :meth:`.traffic_generator.TrafficGenerator.setup`.
+
+        Brings up the port links.
+        """
+        super().setup(ports)
+        self._tg_node.main_session.bring_up_link(ports)
+
     def start_application(self) -> None:
         """Extends :meth:`framework.remote_session.interactive_shell.start_application`.
 
diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py
index 9b4d5dc80a..4469273e36 100644
--- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py
+++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py
@@ -9,10 +9,11 @@
 """
 
 from abc import ABC, abstractmethod
+from typing import Iterable
 
 from scapy.packet import Packet
 
-from framework.config.node import TrafficGeneratorConfig
+from framework.config.test_run import TrafficGeneratorConfig
 from framework.logger import DTSLogger, get_dts_logger
 from framework.testbed_model.node import Node
 from framework.testbed_model.port import Port
@@ -49,6 +50,12 @@ def __init__(self, tg_node: Node, config: TrafficGeneratorConfig, **kwargs):
         self._logger = get_dts_logger(f"{self._tg_node.name} {self._config.type}")
         super().__init__(**kwargs)
 
+    def setup(self, ports: Iterable[Port]):
+        """Setup the traffic generator."""
+
+    def teardown(self):
+        """Teardown the traffic generator."""
+
     def send_packet(self, packet: Packet, port: Port) -> None:
         """Send `packet` and block until it is fully sent.
 
diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py
index 8a5799c684..a8ea07595f 100644
--- a/dts/tests/TestSuite_smoke_tests.py
+++ b/dts/tests/TestSuite_smoke_tests.py
@@ -47,7 +47,7 @@ def set_up_suite(self) -> None:
             Set the build directory path and a list of NICs in the SUT node.
         """
         self.sut_node = self._ctx.sut_node  # FIXME: accessing the context should be forbidden
-        self.dpdk_build_dir_path = self.sut_node.remote_dpdk_build_dir
+        self.dpdk_build_dir_path = self._ctx.dpdk.build.remote_dpdk_build_dir
         self.nics_in_node = self.sut_node.config.ports
 
     @func_test
@@ -79,7 +79,7 @@ def test_driver_tests(self) -> None:
             Run the ``driver-tests`` unit test suite through meson.
         """
         vdev_args = ""
-        for dev in self.sut_node.virtual_devices:
+        for dev in self._ctx.dpdk.get_virtual_devices():
             vdev_args += f"--vdev {dev} "
         vdev_args = vdev_args[:-1]
         driver_tests_command = f"meson test -C {self.dpdk_build_dir_path} --suite driver-tests"
@@ -125,7 +125,7 @@ def test_device_bound_to_driver(self) -> None:
             List all devices with the ``dpdk-devbind.py`` script and verify that
             the configured devices are bound to the proper driver.
         """
-        path_to_devbind = self.sut_node.path_to_devbind_script
+        path_to_devbind = self._ctx.dpdk.devbind_script_path
 
         all_nics_in_dpdk_devbind = self.sut_node.main_session.send_command(
             f"{path_to_devbind} --status | awk '/{REGEX_FOR_PCI_ADDRESS}/'",
diff --git a/dts/tests/TestSuite_softnic.py b/dts/tests/TestSuite_softnic.py
index 370fd6b419..eefd6d3273 100644
--- a/dts/tests/TestSuite_softnic.py
+++ b/dts/tests/TestSuite_softnic.py
@@ -46,7 +46,7 @@ def prepare_softnic_files(self) -> PurePath:
         spec_file = Path("rx_tx.spec")
         rx_tx_1_file = Path("rx_tx_1.io")
         rx_tx_2_file = Path("rx_tx_2.io")
-        path_sut = self.sut_node.remote_dpdk_build_dir
+        path_sut = self._ctx.dpdk.build.remote_dpdk_build_dir
         cli_file_sut = self.sut_node.main_session.join_remote_path(path_sut, cli_file)
         spec_file_sut = self.sut_node.main_session.join_remote_path(path_sut, spec_file)
         rx_tx_1_file_sut = self.sut_node.main_session.join_remote_path(path_sut, rx_tx_1_file)
-- 
2.43.0


  parent reply	other threads:[~2025-02-12 16:46 UTC|newest]

Thread overview: 33+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-02-03 15:16 [RFC PATCH 0/7] dts: revamp framework Luca Vizzarro
2025-02-03 15:16 ` [RFC PATCH 1/7] dts: add port topology configuration Luca Vizzarro
2025-02-07 18:25   ` Nicholas Pratte
2025-02-12 16:47     ` Luca Vizzarro
2025-02-11 18:00   ` Dean Marx
2025-02-12 16:47     ` Luca Vizzarro
2025-02-03 15:16 ` [RFC PATCH 2/7] dts: isolate test specification to config Luca Vizzarro
2025-02-10 19:09   ` Nicholas Pratte
2025-02-11 18:11   ` Dean Marx
2025-02-03 15:16 ` [RFC PATCH 3/7] dts: revamp Topology model Luca Vizzarro
2025-02-10 19:42   ` Nicholas Pratte
2025-02-11 18:18   ` Dean Marx
2025-02-03 15:16 ` [RFC PATCH 4/7] dts: improve Port model Luca Vizzarro
2025-02-11 18:56   ` Dean Marx
2025-02-03 15:16 ` [RFC PATCH 5/7] dts: add runtime status Luca Vizzarro
2025-02-11 19:45   ` Dean Marx
2025-02-12 18:50   ` Nicholas Pratte
2025-02-03 15:16 ` [RFC PATCH 6/7] dts: add global runtime context Luca Vizzarro
2025-02-11 20:26   ` Dean Marx
2025-02-03 15:16 ` [RFC PATCH 7/7] dts: revamp runtime internals Luca Vizzarro
2025-02-11 20:50   ` Dean Marx
2025-02-04 21:08 ` [RFC PATCH 0/7] dts: revamp framework Dean Marx
2025-02-12 16:52   ` Luca Vizzarro
2025-02-12 16:45 ` [PATCH v2 " Luca Vizzarro
2025-02-12 16:45   ` [PATCH v2 1/7] dts: add port topology configuration Luca Vizzarro
2025-02-12 16:45   ` [PATCH v2 2/7] dts: isolate test specification to config Luca Vizzarro
2025-02-12 16:45   ` [PATCH v2 3/7] dts: revamp Topology model Luca Vizzarro
2025-02-12 16:45   ` [PATCH v2 4/7] dts: improve Port model Luca Vizzarro
2025-02-12 16:45   ` [PATCH v2 5/7] dts: add global runtime context Luca Vizzarro
2025-02-12 19:45     ` Nicholas Pratte
2025-02-12 16:45   ` [PATCH v2 6/7] dts: revamp runtime internals Luca Vizzarro
2025-02-12 16:46   ` Luca Vizzarro [this message]
2025-02-12 16:47   ` [PATCH v2 0/7] dts: revamp framework Luca Vizzarro

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250212164600.23759-8-luca.vizzarro@arm.com \
    --to=luca.vizzarro@arm.com \
    --cc=dev@dpdk.org \
    --cc=dmarx@iol.unh.edu \
    --cc=npratte@iol.unh.edu \
    --cc=paul.szczepanek@arm.com \
    --cc=probb@iol.unh.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).