Reviewed-by: Nicholas Pratte <npra...@iol.unh.edu>

On Mon, Oct 28, 2024 at 1:51 PM Luca Vizzarro <luca.vizza...@arm.com> wrote:
>
> This change brings in pydantic in place of warlock. Pydantic offers
> a built-in model validation system in the classes, which allows for
> a more resilient and simpler code. As a consequence of this change:
>
> - most validation is now built-in
> - further validation is added to verify:
>   - cross referencing of node names and ports
>   - test suite and test cases names
> - dictionaries representing the config schema are removed
> - the config schema is no longer used and therefore dropped
> - the TrafficGeneratorType enum has been changed from inheriting
>   StrEnum to the native str and Enum. This change was necessary to
>   enable the discriminator for object unions
> - the structure of the classes has been slightly changed to perfectly
>   match the structure of the configuration files
> - the test suite argument catches the ValidationError that
>   TestSuiteConfig can now raise
> - the DPDK location has been wrapped under another configuration
>   mapping `dpdk_location`
> - the DPDK locations are now structured and enforced by classes,
>   further simplifying the validation and handling thanks to
>   pattern matching
>
> Bugzilla ID: 1508
>
> Signed-off-by: Luca Vizzarro <luca.vizza...@arm.com>
> Reviewed-by: Paul Szczepanek <paul.szczepa...@arm.com>
> ---
>  doc/api/dts/conf_yaml_schema.json             |   1 -
>  doc/api/dts/framework.config.rst              |   6 -
>  doc/api/dts/framework.config.types.rst        |   8 -
>  dts/conf.yaml                                 |  11 +-
>  dts/framework/config/__init__.py              | 822 +++++++++---------
>  dts/framework/config/conf_yaml_schema.json    | 459 ----------
>  dts/framework/config/types.py                 | 149 ----
>  dts/framework/runner.py                       |  57 +-
>  dts/framework/settings.py                     | 124 +--
>  dts/framework/testbed_model/node.py           |  15 +-
>  dts/framework/testbed_model/os_session.py     |   4 +-
>  dts/framework/testbed_model/port.py           |   4 +-
>  dts/framework/testbed_model/posix_session.py  |  10 +-
>  dts/framework/testbed_model/sut_node.py       | 182 ++--
>  dts/framework/testbed_model/topology.py       |  11 +-
>  .../traffic_generator/__init__.py             |   4 +-
>  .../traffic_generator/traffic_generator.py    |   2 +-
>  dts/framework/utils.py                        |   2 +-
>  dts/tests/TestSuite_smoke_tests.py            |   2 +-
>  19 files changed, 653 insertions(+), 1220 deletions(-)
>  delete mode 120000 doc/api/dts/conf_yaml_schema.json
>  delete mode 100644 doc/api/dts/framework.config.types.rst
>  delete mode 100644 dts/framework/config/conf_yaml_schema.json
>  delete mode 100644 dts/framework/config/types.py
>
> diff --git a/doc/api/dts/conf_yaml_schema.json 
> b/doc/api/dts/conf_yaml_schema.json
> deleted file mode 120000
> index 5978642d76..0000000000
> --- a/doc/api/dts/conf_yaml_schema.json
> +++ /dev/null
> @@ -1 +0,0 @@
> -../../../dts/framework/config/conf_yaml_schema.json
> \ No newline at end of file
> diff --git a/doc/api/dts/framework.config.rst 
> b/doc/api/dts/framework.config.rst
> index 261997aefa..cc266276c1 100644
> --- a/doc/api/dts/framework.config.rst
> +++ b/doc/api/dts/framework.config.rst
> @@ -6,9 +6,3 @@ config - Configuration Package
>  .. automodule:: framework.config
>     :members:
>     :show-inheritance:
> -
> -.. toctree::
> -   :hidden:
> -   :maxdepth: 1
> -
> -   framework.config.types
> diff --git a/doc/api/dts/framework.config.types.rst 
> b/doc/api/dts/framework.config.types.rst
> deleted file mode 100644
> index a50a0c874a..0000000000
> --- a/doc/api/dts/framework.config.types.rst
> +++ /dev/null
> @@ -1,8 +0,0 @@
> -.. SPDX-License-Identifier: BSD-3-Clause
> -
> -config.types - Configuration Types
> -==================================
> -
> -.. automodule:: framework.config.types
> -   :members:
> -   :show-inheritance:
> diff --git a/dts/conf.yaml b/dts/conf.yaml
> index 8a65a481d6..2496262854 100644
> --- a/dts/conf.yaml
> +++ b/dts/conf.yaml
> @@ -5,11 +5,12 @@
>  test_runs:
>    # define one test run environment
>    - dpdk_build:
> -      # dpdk_tree: Commented out because `tarball` is defined.
> -      tarball: dpdk-tarball.tar.xz
> -      # Either `dpdk_tree` or `tarball` can be defined, but not both.
> -      remote: false # Optional, defaults to false. If it's true, the 
> `dpdk_tree` or `tarball`
> -                    # is located on the SUT node, instead of the execution 
> host.
> +      dpdk_location:
> +        # dpdk_tree: Commented out because `tarball` is defined.
> +        tarball: dpdk-tarball.tar.xz
> +        # Either `dpdk_tree` or `tarball` can be defined, but not both.
> +        remote: false # Optional, defaults to false. If it's true, the 
> `dpdk_tree` or `tarball`
> +                      # is located on the SUT node, instead of the execution 
> host.
>
>        # precompiled_build_dir: Commented out because `build_options` is 
> defined.
>        build_options:
> diff --git a/dts/framework/config/__init__.py 
> b/dts/framework/config/__init__.py
> index 7403ccbf14..c86bfaaabf 100644
> --- a/dts/framework/config/__init__.py
> +++ b/dts/framework/config/__init__.py
> @@ -2,17 +2,18 @@
>  # Copyright(c) 2010-2021 Intel Corporation
>  # Copyright(c) 2022-2023 University of New Hampshire
>  # Copyright(c) 2023 PANTHEON.tech s.r.o.
> +# Copyright(c) 2024 Arm Limited
>
>  """Testbed configuration and test suite specification.
>
>  This package offers classes that hold real-time information about the 
> testbed, hold test run
>  configuration describing the tested testbed and a loader function, 
> :func:`load_config`, which loads
> -the YAML test run configuration file
> -and validates it according to :download:`the schema <conf_yaml_schema.json>`.
> +the YAML test run configuration file and validates it against the 
> :class:`Configuration` Pydantic
> +model.
>
>  The YAML test run configuration file is parsed into a dictionary, parts of 
> which are used throughout
> -this package. The allowed keys and types inside this dictionary are defined 
> in
> -the :doc:`types <framework.config.types>` module.
> +this package. The allowed keys and types inside this dictionary map directly 
> to the
> +:class:`Configuration` model, its fields and sub-models.
>
>  The test run configuration has two main sections:
>
> @@ -24,39 +25,28 @@
>
>  The real-time information about testbed is supposed to be gathered at 
> runtime.
>
> -The classes defined in this package make heavy use of :mod:`dataclasses`.
> -All of them use slots and are frozen:
> +The classes defined in this package make heavy use of :mod:`pydantic`.
> +Nearly all of them are frozen:
>
> -    * Slots enables some optimizations, by pre-allocating space for the 
> defined
> -      attributes in the underlying data structure,
>      * Frozen makes the object immutable. This enables further optimizations,
>        and makes it thread safe should we ever want to move in that direction.
>  """
>
> -import json
> -import os.path
>  import tarfile
> -from dataclasses import dataclass, fields
> -from enum import auto, unique
> -from pathlib import Path
> -from typing import Union
> +from enum import Enum, auto, unique
> +from functools import cached_property
> +from pathlib import Path, PurePath
> +from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple
>
> -import warlock  # type: ignore[import-untyped]
>  import yaml
> +from pydantic import BaseModel, Field, ValidationError, field_validator, 
> model_validator
>  from typing_extensions import Self
>
> -from framework.config.types import (
> -    ConfigurationDict,
> -    DPDKBuildConfigDict,
> -    DPDKConfigurationDict,
> -    NodeConfigDict,
> -    PortConfigDict,
> -    TestRunConfigDict,
> -    TestSuiteConfigDict,
> -    TrafficGeneratorConfigDict,
> -)
>  from framework.exception import ConfigurationError
> -from framework.utils import StrEnum
> +from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum
> +
> +if TYPE_CHECKING:
> +    from framework.test_suite import TestSuiteSpec
>
>
>  @unique
> @@ -118,15 +108,14 @@ class Compiler(StrEnum):
>
>
>  @unique
> -class TrafficGeneratorType(StrEnum):
> +class TrafficGeneratorType(str, Enum):
>      """The supported traffic generators."""
>
>      #:
> -    SCAPY = auto()
> +    SCAPY = "SCAPY"
>
>
> -@dataclass(slots=True, frozen=True)
> -class HugepageConfiguration:
> +class HugepageConfiguration(BaseModel, frozen=True, extra="forbid"):
>      r"""The hugepage configuration of 
> :class:`~framework.testbed_model.node.Node`\s.
>
>      Attributes:
> @@ -138,12 +127,10 @@ class HugepageConfiguration:
>      force_first_numa: bool
>
>
> -@dataclass(slots=True, frozen=True)
> -class PortConfig:
> +class PortConfig(BaseModel, frozen=True, extra="forbid"):
>      r"""The port configuration of 
> :class:`~framework.testbed_model.node.Node`\s.
>
>      Attributes:
> -        node: The :class:`~framework.testbed_model.node.Node` where this 
> port exists.
>          pci: The PCI address of the port.
>          os_driver_for_dpdk: The operating system driver name for use with 
> DPDK.
>          os_driver: The operating system driver name when the operating 
> system controls the port.
> @@ -152,70 +139,57 @@ class PortConfig:
>          peer_pci: The PCI address of the port connected to this port.
>      """
>
> -    node: str
> -    pci: str
> -    os_driver_for_dpdk: str
> -    os_driver: str
> -    peer_node: str
> -    peer_pci: str
> -
> -    @classmethod
> -    def from_dict(cls, node: str, d: PortConfigDict) -> Self:
> -        """A convenience method that creates the object from fewer inputs.
> -
> -        Args:
> -            node: The node where this port exists.
> -            d: The configuration dictionary.
> -
> -        Returns:
> -            The port configuration instance.
> -        """
> -        return cls(node=node, **d)
> -
> -
> -@dataclass(slots=True, frozen=True)
> -class TrafficGeneratorConfig:
> -    """The configuration of traffic generators.
> -
> -    The class will be expanded when more configuration is needed.
> +    pci: str = Field(
> +        description="The local PCI address of the port.", 
> pattern=REGEX_FOR_PCI_ADDRESS
> +    )
> +    os_driver_for_dpdk: str = Field(
> +        description="The driver that the kernel should bind this device to 
> for DPDK to use it.",
> +        examples=["vfio-pci", "mlx5_core"],
> +    )
> +    os_driver: str = Field(
> +        description="The driver normally used by this port", 
> examples=["i40e", "ice", "mlx5_core"]
> +    )
> +    peer_node: str = Field(description="The name of the peer node this port 
> is connected to.")
> +    peer_pci: str = Field(
> +        description="The PCI address of the peer port this port is connected 
> to.",
> +        pattern=REGEX_FOR_PCI_ADDRESS,
> +    )
> +
> +
> +class TrafficGeneratorConfig(BaseModel, frozen=True, extra="forbid"):
> +    """A protocol required to define traffic generator types.
>
>      Attributes:
> -        traffic_generator_type: The type of the traffic generator.
> +        type: The traffic generator type, the child class is required to 
> define to be distinguished
> +            among others.
>      """
>
> -    traffic_generator_type: TrafficGeneratorType
> +    type: TrafficGeneratorType
>
> -    @staticmethod
> -    def from_dict(d: TrafficGeneratorConfigDict) -> "TrafficGeneratorConfig":
> -        """A convenience method that produces traffic generator config of 
> the proper type.
>
> -        Args:
> -            d: The configuration dictionary.
> +class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig, frozen=True, 
> extra="forbid"):
> +    """Scapy traffic generator specific configuration."""
>
> -        Returns:
> -            The traffic generator configuration instance.
> +    type: Literal[TrafficGeneratorType.SCAPY]
>
> -        Raises:
> -            ConfigurationError: An unknown traffic generator type was 
> encountered.
> -        """
> -        match TrafficGeneratorType(d["type"]):
> -            case TrafficGeneratorType.SCAPY:
> -                return ScapyTrafficGeneratorConfig(
> -                    traffic_generator_type=TrafficGeneratorType.SCAPY
> -                )
> -            case _:
> -                raise ConfigurationError(f'Unknown traffic generator type 
> "{d["type"]}".')
>
> +#: A union type discriminating traffic generators by the `type` field.
> +TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, 
> Field(discriminator="type")]
>
> -@dataclass(slots=True, frozen=True)
> -class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig):
> -    """Scapy traffic generator specific configuration."""
>
> -    pass
> +#: A field representing logical core ranges.
> +LogicalCores = Annotated[
> +    str,
> +    Field(
> +        description="Comma-separated list of logical cores to use. "
> +        "An empty string means use all lcores.",
> +        examples=["1,2,3,4,5,18-22", "10-15"],
> +        pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$",
> +    ),
> +]
>
>
> -@dataclass(slots=True, frozen=True)
> -class NodeConfiguration:
> +class NodeConfiguration(BaseModel, frozen=True, extra="forbid"):
>      r"""The configuration of :class:`~framework.testbed_model.node.Node`\s.
>
>      Attributes:
> @@ -234,285 +208,317 @@ class NodeConfiguration:
>          ports: The ports that can be used in testing.
>      """
>
> -    name: str
> -    hostname: str
> -    user: str
> -    password: str | None
> +    name: str = Field(description="A unique identifier for this node.")
> +    hostname: str = Field(description="The hostname or IP address of the 
> node.")
> +    user: str = Field(description="The login user to use to connect to this 
> node.")
> +    password: str | None = Field(
> +        default=None,
> +        description="The login password to use to connect to this node. "
> +        "SSH keys are STRONGLY preferred, use only as last resort.",
> +    )
>      arch: Architecture
>      os: OS
> -    lcores: str
> -    use_first_core: bool
> -    hugepages: HugepageConfiguration | None
> -    ports: list[PortConfig]
> -
> -    @staticmethod
> -    def from_dict(
> -        d: NodeConfigDict,
> -    ) -> Union["SutNodeConfiguration", "TGNodeConfiguration"]:
> -        """A convenience method that processes the inputs before creating a 
> specialized instance.
> -
> -        Args:
> -            d: The configuration dictionary.
> -
> -        Returns:
> -            Either an SUT or TG configuration instance.
> -        """
> -        hugepage_config = None
> -        if "hugepages_2mb" in d:
> -            hugepage_config_dict = d["hugepages_2mb"]
> -            if "force_first_numa" not in hugepage_config_dict:
> -                hugepage_config_dict["force_first_numa"] = False
> -            hugepage_config = HugepageConfiguration(**hugepage_config_dict)
> -
> -        # The calls here contain duplicated code which is here because Mypy 
> doesn't
> -        # properly support dictionary unpacking with TypedDicts
> -        if "traffic_generator" in d:
> -            return TGNodeConfiguration(
> -                name=d["name"],
> -                hostname=d["hostname"],
> -                user=d["user"],
> -                password=d.get("password"),
> -                arch=Architecture(d["arch"]),
> -                os=OS(d["os"]),
> -                lcores=d.get("lcores", "1"),
> -                use_first_core=d.get("use_first_core", False),
> -                hugepages=hugepage_config,
> -                ports=[PortConfig.from_dict(d["name"], port) for port in 
> d["ports"]],
> -                
> traffic_generator=TrafficGeneratorConfig.from_dict(d["traffic_generator"]),
> -            )
> -        else:
> -            return SutNodeConfiguration(
> -                name=d["name"],
> -                hostname=d["hostname"],
> -                user=d["user"],
> -                password=d.get("password"),
> -                arch=Architecture(d["arch"]),
> -                os=OS(d["os"]),
> -                lcores=d.get("lcores", "1"),
> -                use_first_core=d.get("use_first_core", False),
> -                hugepages=hugepage_config,
> -                ports=[PortConfig.from_dict(d["name"], port) for port in 
> d["ports"]],
> -                memory_channels=d.get("memory_channels", 1),
> -            )
> +    lcores: LogicalCores = "1"
> +    use_first_core: bool = Field(
> +        default=False, description="DPDK won't use the first physical core 
> if set to False."
> +    )
> +    hugepages: HugepageConfiguration | None = Field(None, 
> alias="hugepages_2mb")
> +    ports: list[PortConfig] = Field(min_length=1)
>
>
> -@dataclass(slots=True, frozen=True)
> -class SutNodeConfiguration(NodeConfiguration):
> +class SutNodeConfiguration(NodeConfiguration, frozen=True, extra="forbid"):
>      """:class:`~framework.testbed_model.sut_node.SutNode` specific 
> configuration.
>
>      Attributes:
>          memory_channels: The number of memory channels to use when running 
> DPDK.
>      """
>
> -    memory_channels: int
> +    memory_channels: int = Field(
> +        default=1, description="Number of memory channels to use when 
> running DPDK."
> +    )
>
>
> -@dataclass(slots=True, frozen=True)
> -class TGNodeConfiguration(NodeConfiguration):
> +class TGNodeConfiguration(NodeConfiguration, frozen=True, extra="forbid"):
>      """:class:`~framework.testbed_model.tg_node.TGNode` specific 
> configuration.
>
>      Attributes:
>          traffic_generator: The configuration of the traffic generator 
> present on the TG node.
>      """
>
> -    traffic_generator: TrafficGeneratorConfig
> +    traffic_generator: TrafficGeneratorConfigTypes
> +
> +
> +#: Union type for all the node configuration types.
> +NodeConfigurationTypes = TGNodeConfiguration | SutNodeConfiguration
>
>
> -@dataclass(slots=True, frozen=True)
> -class DPDKBuildConfiguration:
> -    """DPDK build configuration.
> +def resolve_path(path: Path) -> Path:
> +    """Resolve a path into a real path."""
> +    return path.resolve()
>
> -    The configuration used for building DPDK.
> +
> +class BaseDPDKLocation(BaseModel, frozen=True, extra="forbid"):
> +    """DPDK location.
> +
> +    The path to the DPDK sources, build dir and type of location.
>
>      Attributes:
> -        arch: The target architecture to build for.
> -        os: The target os to build for.
> -        cpu: The target CPU to build for.
> -        compiler: The compiler executable to use.
> -        compiler_wrapper: This string will be put in front of the compiler 
> when
> -            executing the build. Useful for adding wrapper commands, such as 
> ``ccache``.
> -        name: The name of the compiler.
> +        remote: Optional, defaults to :data:`False`. If :data:`True`, 
> `dpdk_tree` or `tarball` is
> +            located on the SUT node, instead of the execution host.
>      """
>
> -    arch: Architecture
> -    os: OS
> -    cpu: CPUType
> -    compiler: Compiler
> -    compiler_wrapper: str
> -    name: str
> +    remote: bool = False
>
> -    @classmethod
> -    def from_dict(cls, d: DPDKBuildConfigDict) -> Self:
> -        r"""A convenience method that processes the inputs before creating 
> an instance.
>
> -        `arch`, `os`, `cpu` and `compiler` are converted to :class:`Enum`\s 
> and
> -        `name` is constructed from `arch`, `os`, `cpu` and `compiler`.
> +class LocalDPDKLocation(BaseDPDKLocation, frozen=True, extra="forbid"):
> +    """Local DPDK location parent class.
>
> -        Args:
> -            d: The configuration dictionary.
> +    This class is meant to represent any location that is present only 
> locally.
> +    """
>
> -        Returns:
> -            The DPDK build configuration instance.
> -        """
> -        return cls(
> -            arch=Architecture(d["arch"]),
> -            os=OS(d["os"]),
> -            cpu=CPUType(d["cpu"]),
> -            compiler=Compiler(d["compiler"]),
> -            compiler_wrapper=d.get("compiler_wrapper", ""),
> -            name=f"{d['arch']}-{d['os']}-{d['cpu']}-{d['compiler']}",
> -        )
> +    remote: Literal[False] = False
>
>
> -@dataclass(slots=True, frozen=True)
> -class DPDKLocation:
> -    """DPDK location.
> +class LocalDPDKTreeLocation(LocalDPDKLocation, frozen=True, extra="forbid"):
> +    """Local DPDK tree location.
>
> -    The path to the DPDK sources, build dir and type of location.
> +    This class makes a distinction from :class:`RemoteDPDKTreeLocation` by 
> enforcing on the fly
> +    validation.
>
>      Attributes:
> -        dpdk_tree: The path to the DPDK source tree directory. Only one of 
> `dpdk_tree` or `tarball`
> -            must be provided.
> -        tarball: The path to the DPDK tarball. Only one of `dpdk_tree` or 
> `tarball` must be
> -            provided.
> -        remote: Optional, defaults to :data:`False`. If :data:`True`, 
> `dpdk_tree` or `tarball` is
> -            located on the SUT node, instead of the execution host.
> -        build_dir: If it's defined, DPDK has been pre-compiled and the build 
> directory is located in
> -            a subdirectory of `dpdk_tree` or `tarball` root directory. 
> Otherwise, will be using
> -            `build_options` from configuration to build the DPDK from source.
> +        dpdk_tree: The path to the DPDK source tree directory.
>      """
>
> -    dpdk_tree: str | None
> -    tarball: str | None
> -    remote: bool
> -    build_dir: str | None
> +    dpdk_tree: Path
>
> -    @classmethod
> -    def from_dict(cls, d: DPDKConfigurationDict) -> Self:
> -        """A convenience method that processes and validates the inputs 
> before creating an instance.
> +    #: Resolve the local DPDK tree path
> +    resolve_dpdk_tree_path = field_validator("dpdk_tree")(resolve_path)
>
> -        Validate existence and format of `dpdk_tree` or `tarball` on local 
> filesystem, if
> -        `remote` is False.
> +    @model_validator(mode="after")
> +    def validate_dpdk_tree_path(self) -> Self:
> +        """Validate the provided DPDK tree path."""
> +        assert self.dpdk_tree.exists(), "DPDK tree not found in local 
> filesystem."
> +        assert self.dpdk_tree.is_dir(), "The DPDK tree path must be a 
> directory."
> +        return self
>
> -        Args:
> -            d: The configuration dictionary.
>
> -        Returns:
> -            The DPDK location instance.
> +class LocalDPDKTarballLocation(LocalDPDKLocation, frozen=True, 
> extra="forbid"):
> +    """Local DPDK tarball location.
>
> -        Raises:
> -            ConfigurationError: If `dpdk_tree` or `tarball` not found in 
> local filesystem or they
> -                aren't in the right format.
> -        """
> -        dpdk_tree = d.get("dpdk_tree")
> -        tarball = d.get("tarball")
> -        remote = d.get("remote", False)
> -
> -        if not remote:
> -            if dpdk_tree:
> -                if not Path(dpdk_tree).exists():
> -                    raise ConfigurationError(
> -                        f"DPDK tree '{dpdk_tree}' not found in local 
> filesystem."
> -                    )
> -
> -                if not Path(dpdk_tree).is_dir():
> -                    raise ConfigurationError(f"The DPDK tree '{dpdk_tree}' 
> must be a directory.")
> -
> -                dpdk_tree = os.path.realpath(dpdk_tree)
> -
> -            if tarball:
> -                if not Path(tarball).exists():
> -                    raise ConfigurationError(
> -                        f"DPDK tarball '{tarball}' not found in local 
> filesystem."
> -                    )
> -
> -                if not tarfile.is_tarfile(tarball):
> -                    raise ConfigurationError(
> -                        f"The DPDK tarball '{tarball}' must be a valid tar 
> archive."
> -                    )
> -
> -        return cls(
> -            dpdk_tree=dpdk_tree,
> -            tarball=tarball,
> -            remote=remote,
> -            build_dir=d.get("precompiled_build_dir"),
> -        )
> +    This class makes a distinction from :class:`RemoteDPDKTarballLocation` 
> by enforcing on the fly
> +    validation.
> +
> +    Attributes:
> +        tarball: The path to the DPDK tarball.
> +    """
>
> +    tarball: Path
>
> -@dataclass
> -class DPDKConfiguration:
> -    """The configuration of the DPDK build.
> +    #: Resolve the local tarball path
> +    resolve_tarball_path = field_validator("tarball")(resolve_path)
>
> -    The configuration contain the location of the DPDK and configuration 
> used for
> -    building it.
> +    @model_validator(mode="after")
> +    def validate_tarball_path(self) -> Self:
> +        """Validate the provided tarball."""
> +        assert self.tarball.exists(), "DPDK tarball not found in local 
> filesystem."
> +        assert tarfile.is_tarfile(self.tarball), "The DPDK tarball must be a 
> valid tar archive."
> +        return self
> +
> +
> +class RemoteDPDKLocation(BaseDPDKLocation, frozen=True, extra="forbid"):
> +    """Remote DPDK location parent class.
> +
> +    This class is meant to represent any location that is present only 
> remotely.
> +    """
> +
> +    remote: Literal[True] = True
> +
> +
> +class RemoteDPDKTreeLocation(RemoteDPDKLocation, frozen=True, 
> extra="forbid"):
> +    """Remote DPDK tree location.
> +
> +    This class is distinct from :class:`LocalDPDKTreeLocation` which 
> enforces on the fly validation.
> +
> +    Attributes:
> +        dpdk_tree: The path to the DPDK source tree directory.
> +    """
> +
> +    dpdk_tree: PurePath
> +
> +
> +class RemoteDPDKTarballLocation(LocalDPDKLocation, frozen=True, 
> extra="forbid"):
> +    """Remote DPDK tarball location.
> +
> +    This class is distinct from :class:`LocalDPDKTarballLocation` which 
> enforces on the fly
> +    validation.
> +
> +    Attributes:
> +        tarball: The path to the DPDK tarball.
> +    """
> +
> +    tarball: PurePath
> +
> +
> +#: Union type for different DPDK locations
> +DPDKLocation = (
> +    LocalDPDKTreeLocation
> +    | LocalDPDKTarballLocation
> +    | RemoteDPDKTreeLocation
> +    | RemoteDPDKTarballLocation
> +)
> +
> +
> +class BaseDPDKBuildConfiguration(BaseModel, frozen=True, extra="forbid"):
> +    """The base configuration for different types of build.
> +
> +    The configuration contain the location of the DPDK and configuration 
> used for building it.
>
>      Attributes:
>          dpdk_location: The location of the DPDK tree.
> -        dpdk_build_config: A DPDK build configuration to test. If 
> :data:`None`,
> -            DTS will use pre-built DPDK from `build_dir` in a 
> :class:`DPDKLocation`.
>      """
>
>      dpdk_location: DPDKLocation
> -    dpdk_build_config: DPDKBuildConfiguration | None
>
> -    @classmethod
> -    def from_dict(cls, d: DPDKConfigurationDict) -> Self:
> -        """A convenience method that processes the inputs before creating an 
> instance.
>
> -        Args:
> -            d: The configuration dictionary.
> +class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration, 
> frozen=True, extra="forbid"):
> +    """DPDK precompiled build configuration.
>
> -        Returns:
> -            The DPDK configuration.
> -        """
> -        return cls(
> -            dpdk_location=DPDKLocation.from_dict(d),
> -            dpdk_build_config=(
> -                DPDKBuildConfiguration.from_dict(d["build_options"])
> -                if d.get("build_options")
> -                else None
> -            ),
> -        )
> +    Attributes:
> +        precompiled_build_dir: If it's defined, DPDK has been pre-compiled 
> and the build directory
> +            is located in a subdirectory of `dpdk_tree` or `tarball` root 
> directory. Otherwise, will
> +            be using `dpdk_build_config` from configuration to build the 
> DPDK from source.
> +    """
> +
> +    precompiled_build_dir: str = Field(min_length=1)
> +
> +
> +class DPDKBuildOptionsConfiguration(BaseModel, frozen=True, extra="forbid"):
> +    """DPDK build options configuration.
> +
> +    The build options used for building DPDK.
> +
> +    Attributes:
> +        arch: The target architecture to build for.
> +        os: The target os to build for.
> +        cpu: The target CPU to build for.
> +        compiler: The compiler executable to use.
> +        compiler_wrapper: This string will be put in front of the compiler 
> when executing the build.
> +            Useful for adding wrapper commands, such as ``ccache``.
> +    """
> +
> +    arch: Architecture
> +    os: OS
> +    cpu: CPUType
> +    compiler: Compiler
> +    compiler_wrapper: str = ""
>
> +    @cached_property
> +    def name(self) -> str:
> +        """The name of the compiler."""
> +        return f"{self.arch}-{self.os}-{self.cpu}-{self.compiler}"
>
> -@dataclass(slots=True, frozen=True)
> -class TestSuiteConfig:
> +
> +class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration, 
> frozen=True, extra="forbid"):
> +    """DPDK uncompiled build configuration.
> +
> +    Attributes:
> +        build_options: The build options to compile DPDK.
> +    """
> +
> +    build_options: DPDKBuildOptionsConfiguration
> +
> +
> +#: Union type for different build configurations
> +DPDKBuildConfiguration = DPDKPrecompiledBuildConfiguration | 
> DPDKUncompiledBuildConfiguration
> +
> +
> +class TestSuiteConfig(BaseModel, frozen=True, extra="forbid"):
>      """Test suite configuration.
>
> -    Information about a single test suite to be executed.
> +    Information about a single test suite to be executed. This can also be 
> represented as a string
> +    instead of a mapping, example:
> +
> +    .. code:: yaml
> +
> +        test_runs:
> +        - test_suites:
> +            # As string representation:
> +            - hello_world # test all of `hello_world`, or
> +            - hello_world hello_world_single_core # test only 
> `hello_world_single_core`
> +            # or as model fields:
> +            - test_suite: hello_world
> +              test_cases: [hello_world_single_core] # without this field all 
> test cases are run
>
>      Attributes:
> -        test_suite: The name of the test suite module without the starting 
> ``TestSuite_``.
> -        test_cases: The names of test cases from this test suite to execute.
> +        test_suite_name: The name of the test suite module without the 
> starting ``TestSuite_``.
> +        test_cases_names: The names of test cases from this test suite to 
> execute.
>              If empty, all test cases will be executed.
>      """
>
> -    test_suite: str
> -    test_cases: list[str]
> -
> +    test_suite_name: str = Field(
> +        title="Test suite name",
> +        description="The identifying module name of the test suite without 
> the prefix.",
> +        alias="test_suite",
> +    )
> +    test_cases_names: list[str] = Field(
> +        default_factory=list,
> +        title="Test cases by name",
> +        description="The identifying name of the test cases of the test 
> suite.",
> +        alias="test_cases",
> +    )
> +
> +    @cached_property
> +    def test_suite_spec(self) -> "TestSuiteSpec":
> +        """The specification of the requested test suite."""
> +        from framework.test_suite import find_by_name
> +
> +        test_suite_spec = find_by_name(self.test_suite_name)
> +        assert (
> +            test_suite_spec is not None
> +        ), f"{self.test_suite_name} is not a valid test suite module name."
> +        return test_suite_spec
> +
> +    @model_validator(mode="before")
>      @classmethod
> -    def from_dict(
> -        cls,
> -        entry: str | TestSuiteConfigDict,
> -    ) -> Self:
> -        """Create an instance from two different types.
> +    def convert_from_string(cls, data: Any) -> Any:
> +        """Convert the string representation of the model into a valid 
> mapping."""
> +        if isinstance(data, str):
> +            [test_suite, *test_cases] = data.split()
> +            return dict(test_suite=test_suite, test_cases=test_cases)
> +        return data
> +
> +    @model_validator(mode="after")
> +    def validate_names(self) -> Self:
> +        """Validate the supplied test suite and test cases names.
> +
> +        This validator relies on the cached property `test_suite_spec` to 
> run for the first
> +        time in this call, therefore triggering the assertions if needed.
> +        """
> +        available_test_cases = map(
> +            lambda t: t.name, self.test_suite_spec.class_obj.get_test_cases()
> +        )
> +        for requested_test_case in self.test_cases_names:
> +            assert requested_test_case in available_test_cases, (
> +                f"{requested_test_case} is not a valid test case "
> +                f"of test suite {self.test_suite_name}."
> +            )
>
> -        Args:
> -            entry: Either a suite name or a dictionary containing the config.
> +        return self
>
> -        Returns:
> -            The test suite configuration instance.
> -        """
> -        if isinstance(entry, str):
> -            return cls(test_suite=entry, test_cases=[])
> -        elif isinstance(entry, dict):
> -            return cls(test_suite=entry["suite"], test_cases=entry["cases"])
> -        else:
> -            raise TypeError(f"{type(entry)} is not valid for a test suite 
> config.")
>
> +class TestRunSUTNodeConfiguration(BaseModel, frozen=True, extra="forbid"):
> +    """The SUT node configuration of a test run.
>
> -@dataclass(slots=True, frozen=True)
> -class TestRunConfiguration:
> +    Attributes:
> +        node_name: The SUT node to use in this test run.
> +        vdevs: The names of virtual devices to test.
> +    """
> +
> +    node_name: str
> +    vdevs: list[str] = Field(default_factory=list)
> +
> +
> +class TestRunConfiguration(BaseModel, frozen=True, extra="forbid"):
>      """The configuration of a test run.
>
>      The configuration contains testbed information, what tests to execute
> @@ -524,144 +530,130 @@ class TestRunConfiguration:
>          func: Whether to run functional tests.
>          skip_smoke_tests: Whether to skip smoke tests.
>          test_suites: The names of test suites and/or test cases to execute.
> -        system_under_test_node: The SUT node to use in this test run.
> -        traffic_generator_node: The TG node to use in this test run.
> -        vdevs: The names of virtual devices to test.
> +        system_under_test_node: The SUT node configuration to use in this 
> test run.
> +        traffic_generator_node: The TG node name to use in this test run.
>          random_seed: The seed to use for pseudo-random generation.
>      """
>
> -    dpdk_config: DPDKConfiguration
> -    perf: bool
> -    func: bool
> -    skip_smoke_tests: bool
> -    test_suites: list[TestSuiteConfig]
> -    system_under_test_node: SutNodeConfiguration
> -    traffic_generator_node: TGNodeConfiguration
> -    vdevs: list[str]
> -    random_seed: int | None
> -
> -    @classmethod
> -    def from_dict(
> -        cls,
> -        d: TestRunConfigDict,
> -        node_map: dict[str, SutNodeConfiguration | TGNodeConfiguration],
> -    ) -> Self:
> -        """A convenience method that processes the inputs before creating an 
> instance.
> -
> -        The DPDK build and the test suite config are transformed into their 
> respective objects.
> -        SUT and TG configurations are taken from `node_map`. The other 
> (:class:`bool`) attributes
> -        are just stored.
> -
> -        Args:
> -            d: The test run configuration dictionary.
> -            node_map: A dictionary mapping node names to their config 
> objects.
> -
> -        Returns:
> -            The test run configuration instance.
> -        """
> -        test_suites: list[TestSuiteConfig] = 
> list(map(TestSuiteConfig.from_dict, d["test_suites"]))
> -        sut_name = d["system_under_test_node"]["node_name"]
> -        skip_smoke_tests = d.get("skip_smoke_tests", False)
> -        assert sut_name in node_map, f"Unknown SUT {sut_name} in test run 
> {d}"
> -        system_under_test_node = node_map[sut_name]
> -        assert isinstance(
> -            system_under_test_node, SutNodeConfiguration
> -        ), f"Invalid SUT configuration {system_under_test_node}"
> -
> -        tg_name = d["traffic_generator_node"]
> -        assert tg_name in node_map, f"Unknown TG {tg_name} in test run {d}"
> -        traffic_generator_node = node_map[tg_name]
> -        assert isinstance(
> -            traffic_generator_node, TGNodeConfiguration
> -        ), f"Invalid TG configuration {traffic_generator_node}"
> -
> -        vdevs = (
> -            d["system_under_test_node"]["vdevs"] if "vdevs" in 
> d["system_under_test_node"] else []
> -        )
> -        random_seed = d.get("random_seed", None)
> -        return cls(
> -            dpdk_config=DPDKConfiguration.from_dict(d["dpdk_build"]),
> -            perf=d["perf"],
> -            func=d["func"],
> -            skip_smoke_tests=skip_smoke_tests,
> -            test_suites=test_suites,
> -            system_under_test_node=system_under_test_node,
> -            traffic_generator_node=traffic_generator_node,
> -            vdevs=vdevs,
> -            random_seed=random_seed,
> -        )
> -
> -    def copy_and_modify(self, **kwargs) -> Self:
> -        """Create a shallow copy with any of the fields modified.
> +    dpdk_config: DPDKBuildConfiguration = Field(alias="dpdk_build")
> +    perf: bool = Field(description="Enable performance testing.")
> +    func: bool = Field(description="Enable functional testing.")
> +    skip_smoke_tests: bool = False
> +    test_suites: list[TestSuiteConfig] = Field(min_length=1)
> +    system_under_test_node: TestRunSUTNodeConfiguration
> +    traffic_generator_node: str
> +    random_seed: int | None = None
>
> -        The only new data are those passed to this method.
> -        The rest are copied from the object's fields calling the method.
>
> -        Args:
> -            **kwargs: The names and types of keyword arguments are defined
> -                by the fields of the :class:`TestRunConfiguration` class.
> +class TestRunWithNodesConfiguration(NamedTuple):
> +    """Tuple containing the configuration of the test run and its associated 
> nodes."""
>
> -        Returns:
> -            The copied and modified test run configuration.
> -        """
> -        new_config = {}
> -        for field in fields(self):
> -            if field.name in kwargs:
> -                new_config[field.name] = kwargs[field.name]
> -            else:
> -                new_config[field.name] = getattr(self, field.name)
> -
> -        return type(self)(**new_config)
> +    #:
> +    test_run_config: TestRunConfiguration
> +    #:
> +    sut_node_config: SutNodeConfiguration
> +    #:
> +    tg_node_config: TGNodeConfiguration
>
>
> -@dataclass(slots=True, frozen=True)
> -class Configuration:
> +class Configuration(BaseModel, extra="forbid"):
>      """DTS testbed and test configuration.
>
> -    The node configuration is not stored in this object. Rather, all used 
> node configurations
> -    are stored inside the test run configuration where the nodes are 
> actually used.
> -
>      Attributes:
>          test_runs: Test run configurations.
> +        nodes: Node configurations.
>      """
>
> -    test_runs: list[TestRunConfiguration]
> +    test_runs: list[TestRunConfiguration] = Field(min_length=1)
> +    nodes: list[NodeConfigurationTypes] = Field(min_length=1)
>
> -    @classmethod
> -    def from_dict(cls, d: ConfigurationDict) -> Self:
> -        """A convenience method that processes the inputs before creating an 
> instance.
> +    @cached_property
> +    def test_runs_with_nodes(self) -> list[TestRunWithNodesConfiguration]:
> +        """List of test runs with the associated nodes."""
> +        test_runs_with_nodes = []
>
> -        DPDK build and test suite config are transformed into their 
> respective objects.
> -        SUT and TG configurations are taken from `node_map`. The other 
> (:class:`bool`) attributes
> -        are just stored.
> +        for test_run_no, test_run in enumerate(self.test_runs):
> +            sut_node_name = test_run.system_under_test_node.node_name
> +            sut_node = next(filter(lambda n: n.name == sut_node_name, 
> self.nodes), None)
>
> -        Args:
> -            d: The configuration dictionary.
> +            assert sut_node is not None, (
> +                f"test_runs.{test_run_no}.sut_node_config.node_name "
> +                f"({test_run.system_under_test_node.node_name}) is not a 
> valid node name"
> +            )
> +            assert isinstance(sut_node, SutNodeConfiguration), (
> +                f"test_runs.{test_run_no}.sut_node_config.node_name is a 
> valid node name, "
> +                "but it is not a valid SUT node"
> +            )
>
> -        Returns:
> -            The whole configuration instance.
> -        """
> -        nodes: list[SutNodeConfiguration | TGNodeConfiguration] = list(
> -            map(NodeConfiguration.from_dict, d["nodes"])
> -        )
> -        assert len(nodes) > 0, "There must be a node to test"
> +            tg_node_name = test_run.traffic_generator_node
> +            tg_node = next(filter(lambda n: n.name == tg_node_name, 
> self.nodes), None)
>
> -        node_map = {node.name: node for node in nodes}
> -        assert len(nodes) == len(node_map), "Duplicate node names are not 
> allowed"
> +            assert tg_node is not None, (
> +                f"test_runs.{test_run_no}.tg_node_name "
> +                f"({test_run.traffic_generator_node}) is not a valid node 
> name"
> +            )
> +            assert isinstance(tg_node, TGNodeConfiguration), (
> +                f"test_runs.{test_run_no}.tg_node_name is a valid node name, 
> "
> +                "but it is not a valid TG node"
> +            )
>
> -        test_runs: list[TestRunConfiguration] = list(
> -            map(TestRunConfiguration.from_dict, d["test_runs"], [node_map 
> for _ in d])
> -        )
> +            
> test_runs_with_nodes.append(TestRunWithNodesConfiguration(test_run, sut_node, 
> tg_node))
> +
> +        return test_runs_with_nodes
> +
> +    @field_validator("nodes")
> +    @classmethod
> +    def validate_node_names(cls, nodes: list[NodeConfiguration]) -> 
> list[NodeConfiguration]:
> +        """Validate that the node names are unique."""
> +        nodes_by_name: dict[str, int] = {}
> +        for node_no, node in enumerate(nodes):
> +            assert node.name not in nodes_by_name, (
> +                f"node {node_no} cannot have the same name as node 
> {nodes_by_name[node.name]} "
> +                f"({node.name})"
> +            )
> +            nodes_by_name[node.name] = node_no
> +
> +        return nodes
> +
> +    @model_validator(mode="after")
> +    def validate_ports(self) -> Self:
> +        """Validate that the ports are all linked to valid ones."""
> +        port_links: dict[tuple[str, str], Literal[False] | tuple[int, int]] 
> = {
> +            (node.name, port.pci): False for node in self.nodes for port in 
> node.ports
> +        }
> +
> +        for node_no, node in enumerate(self.nodes):
> +            for port_no, port in enumerate(node.ports):
> +                peer_port_identifier = (port.peer_node, port.peer_pci)
> +                peer_port = port_links.get(peer_port_identifier, None)
> +                assert peer_port is not None, (
> +                    "invalid peer port specified for " 
> f"nodes.{node_no}.ports.{port_no}"
> +                )
> +                assert peer_port is False, (
> +                    f"the peer port specified for 
> nodes.{node_no}.ports.{port_no} "
> +                    f"is already linked to 
> nodes.{peer_port[0]}.ports.{peer_port[1]}"
> +                )
> +                port_links[peer_port_identifier] = (node_no, port_no)
>
> -        return cls(test_runs=test_runs)
> +        return self
> +
> +    @model_validator(mode="after")
> +    def validate_test_runs_with_nodes(self) -> Self:
> +        """Validate the test runs to nodes associations.
> +
> +        This validator relies on the cached property `test_runs_with_nodes` 
> to run for the first
> +        time in this call, therefore triggering the assertions if needed.
> +        """
> +        if self.test_runs_with_nodes:
> +            pass
> +        return self
>
>
>  def load_config(config_file_path: Path) -> Configuration:
>      """Load DTS test run configuration from a file.
>
> -    Load the YAML test run configuration file
> -    and :download:`the configuration file schema <conf_yaml_schema.json>`,
> -    validate the test run configuration file, and create a test run 
> configuration object.
> +    Load the YAML test run configuration file, validate it, and create a 
> test run configuration
> +    object.
>
>      The YAML test run configuration file is specified in the 
> :option:`--config-file` command line
>      argument or the :envvar:`DTS_CFG_FILE` environment variable.
> @@ -671,14 +663,14 @@ def load_config(config_file_path: Path) -> 
> Configuration:
>
>      Returns:
>          The parsed test run configuration.
> +
> +    Raises:
> +        ConfigurationError: If the supplied configuration file is invalid.
>      """
>      with open(config_file_path, "r") as f:
>          config_data = yaml.safe_load(f)
>
> -    schema_path = os.path.join(Path(__file__).parent.resolve(), 
> "conf_yaml_schema.json")
> -
> -    with open(schema_path, "r") as f:
> -        schema = json.load(f)
> -    config = warlock.model_factory(schema, name="_Config")(config_data)
> -    config_obj: Configuration = Configuration.from_dict(dict(config))  # 
> type: ignore[arg-type]
> -    return config_obj
> +    try:
> +        return Configuration.model_validate(config_data)
> +    except ValidationError as e:
> +        raise ConfigurationError("failed to load the supplied 
> configuration") from e
> diff --git a/dts/framework/config/conf_yaml_schema.json 
> b/dts/framework/config/conf_yaml_schema.json
> deleted file mode 100644
> index cc3e78cef5..0000000000
> --- a/dts/framework/config/conf_yaml_schema.json
> +++ /dev/null
> @@ -1,459 +0,0 @@
> -{
> -  "$schema": "https://json-schema.org/draft-07/schema";,
> -  "title": "DTS Config Schema",
> -  "definitions": {
> -    "node_name": {
> -      "type": "string",
> -      "description": "A unique identifier for a node"
> -    },
> -    "NIC": {
> -      "type": "string",
> -      "enum": [
> -        "ALL",
> -        "ConnectX3_MT4103",
> -        "ConnectX4_LX_MT4117",
> -        "ConnectX4_MT4115",
> -        "ConnectX5_MT4119",
> -        "ConnectX5_MT4121",
> -        "I40E_10G-10G_BASE_T_BC",
> -        "I40E_10G-10G_BASE_T_X722",
> -        "I40E_10G-SFP_X722",
> -        "I40E_10G-SFP_XL710",
> -        "I40E_10G-X722_A0",
> -        "I40E_1G-1G_BASE_T_X722",
> -        "I40E_25G-25G_SFP28",
> -        "I40E_40G-QSFP_A",
> -        "I40E_40G-QSFP_B",
> -        "IAVF-ADAPTIVE_VF",
> -        "IAVF-VF",
> -        "IAVF_10G-X722_VF",
> -        "ICE_100G-E810C_QSFP",
> -        "ICE_25G-E810C_SFP",
> -        "ICE_25G-E810_XXV_SFP",
> -        "IGB-I350_VF",
> -        "IGB_1G-82540EM",
> -        "IGB_1G-82545EM_COPPER",
> -        "IGB_1G-82571EB_COPPER",
> -        "IGB_1G-82574L",
> -        "IGB_1G-82576",
> -        "IGB_1G-82576_QUAD_COPPER",
> -        "IGB_1G-82576_QUAD_COPPER_ET2",
> -        "IGB_1G-82580_COPPER",
> -        "IGB_1G-I210_COPPER",
> -        "IGB_1G-I350_COPPER",
> -        "IGB_1G-I354_SGMII",
> -        "IGB_1G-PCH_LPTLP_I218_LM",
> -        "IGB_1G-PCH_LPTLP_I218_V",
> -        "IGB_1G-PCH_LPT_I217_LM",
> -        "IGB_1G-PCH_LPT_I217_V",
> -        "IGB_2.5G-I354_BACKPLANE_2_5GBPS",
> -        "IGC-I225_LM",
> -        "IGC-I226_LM",
> -        "IXGBE_10G-82599_SFP",
> -        "IXGBE_10G-82599_SFP_SF_QP",
> -        "IXGBE_10G-82599_T3_LOM",
> -        "IXGBE_10G-82599_VF",
> -        "IXGBE_10G-X540T",
> -        "IXGBE_10G-X540_VF",
> -        "IXGBE_10G-X550EM_A_SFP",
> -        "IXGBE_10G-X550EM_X_10G_T",
> -        "IXGBE_10G-X550EM_X_SFP",
> -        "IXGBE_10G-X550EM_X_VF",
> -        "IXGBE_10G-X550T",
> -        "IXGBE_10G-X550_VF",
> -        "brcm_57414",
> -        "brcm_P2100G",
> -        "cavium_0011",
> -        "cavium_a034",
> -        "cavium_a063",
> -        "cavium_a064",
> -        "fastlinq_ql41000",
> -        "fastlinq_ql41000_vf",
> -        "fastlinq_ql45000",
> -        "fastlinq_ql45000_vf",
> -        "hi1822",
> -        "virtio"
> -      ]
> -    },
> -
> -    "ARCH": {
> -      "type": "string",
> -      "enum": [
> -        "x86_64",
> -        "arm64",
> -        "ppc64le"
> -      ]
> -    },
> -    "OS": {
> -      "type": "string",
> -      "enum": [
> -        "linux"
> -      ]
> -    },
> -    "cpu": {
> -      "type": "string",
> -      "description": "Native should be the default on x86",
> -      "enum": [
> -        "native",
> -        "armv8a",
> -        "dpaa2",
> -        "thunderx",
> -        "xgene1"
> -      ]
> -    },
> -    "compiler": {
> -      "type": "string",
> -      "enum": [
> -        "gcc",
> -        "clang",
> -        "icc",
> -        "mscv"
> -      ]
> -    },
> -    "build_options": {
> -      "type": "object",
> -      "properties": {
> -        "arch": {
> -          "type": "string",
> -          "enum": [
> -            "ALL",
> -            "x86_64",
> -            "arm64",
> -            "ppc64le",
> -            "other"
> -          ]
> -        },
> -        "os": {
> -          "$ref": "#/definitions/OS"
> -        },
> -        "cpu": {
> -          "$ref": "#/definitions/cpu"
> -        },
> -        "compiler": {
> -          "$ref": "#/definitions/compiler"
> -        },
> -        "compiler_wrapper": {
> -          "type": "string",
> -          "description": "This will be added before compiler to the CC 
> variable when building DPDK. Optional."
> -        }
> -      },
> -      "additionalProperties": false,
> -      "required": [
> -        "arch",
> -        "os",
> -        "cpu",
> -        "compiler"
> -      ]
> -    },
> -    "dpdk_build": {
> -      "type": "object",
> -      "description": "DPDK source and build configuration.",
> -      "properties": {
> -        "dpdk_tree": {
> -          "type": "string",
> -          "description": "The path to the DPDK source tree directory to 
> test. Only one of `dpdk_tree` or `tarball` must be provided."
> -        },
> -        "tarball": {
> -          "type": "string",
> -          "description": "The path to the DPDK source tarball to test. Only 
> one of `dpdk_tree` or `tarball` must be provided."
> -        },
> -        "remote": {
> -          "type": "boolean",
> -          "description": "Optional, defaults to false. If it's true, the 
> `dpdk_tree` or `tarball` is located on the SUT node, instead of the execution 
> host."
> -        },
> -        "precompiled_build_dir": {
> -          "type": "string",
> -          "description": "If it's defined, DPDK has been pre-built and the 
> build directory is located in a subdirectory of DPDK tree root directory. 
> Otherwise, will be using a `build_options` to build the DPDK from source. 
> Either this or `build_options` must be defined, but not both."
> -        },
> -        "build_options": {
> -          "$ref": "#/definitions/build_options",
> -          "description": "Either this or `precompiled_build_dir` must be 
> defined, but not both. DPDK build configuration supported by DTS."
> -        }
> -      },
> -      "allOf": [
> -        {
> -          "oneOf": [
> -            {
> -            "required": [
> -              "dpdk_tree"
> -              ]
> -            },
> -            {
> -              "required": [
> -                "tarball"
> -              ]
> -            }
> -          ]
> -        },
> -        {
> -          "oneOf": [
> -            {
> -              "required": [
> -                "precompiled_build_dir"
> -              ]
> -            },
> -            {
> -              "required": [
> -                "build_options"
> -              ]
> -            }
> -          ]
> -        }
> -      ],
> -      "additionalProperties": false
> -    },
> -    "hugepages_2mb": {
> -      "type": "object",
> -      "description": "Optional hugepage configuration. If not specified, 
> hugepages won't be configured and DTS will use system configuration.",
> -      "properties": {
> -        "number_of": {
> -          "type": "integer",
> -          "description": "The number of hugepages to configure. Hugepage 
> size will be the system default."
> -        },
> -        "force_first_numa": {
> -          "type": "boolean",
> -          "description": "Set to True to force configuring hugepages on the 
> first NUMA node. Defaults to False."
> -        }
> -      },
> -      "additionalProperties": false,
> -      "required": [
> -        "number_of"
> -      ]
> -    },
> -    "mac_address": {
> -      "type": "string",
> -      "description": "A MAC address",
> -      "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
> -    },
> -    "pci_address": {
> -      "type": "string",
> -      "pattern": "^[\\da-fA-F]{4}:[\\da-fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$"
> -    },
> -    "port_peer_address": {
> -      "description": "Peer is a TRex port, and IXIA port or a PCI address",
> -      "oneOf": [
> -        {
> -          "description": "PCI peer port",
> -          "$ref": "#/definitions/pci_address"
> -        }
> -      ]
> -    },
> -    "test_suite": {
> -      "type": "string",
> -      "enum": [
> -        "hello_world",
> -        "os_udp",
> -        "pmd_buffer_scatter",
> -        "vlan"
> -      ]
> -    },
> -    "test_target": {
> -      "type": "object",
> -      "properties": {
> -        "suite": {
> -          "$ref": "#/definitions/test_suite"
> -        },
> -        "cases": {
> -          "type": "array",
> -          "description": "If specified, only this subset of test suite's 
> test cases will be run.",
> -          "items": {
> -            "type": "string"
> -          },
> -          "minimum": 1
> -        }
> -      },
> -      "required": [
> -        "suite"
> -      ],
> -      "additionalProperties": false
> -    }
> -  },
> -  "type": "object",
> -  "properties": {
> -    "nodes": {
> -      "type": "array",
> -      "items": {
> -        "type": "object",
> -        "properties": {
> -          "name": {
> -            "type": "string",
> -            "description": "A unique identifier for this node"
> -          },
> -          "hostname": {
> -            "type": "string",
> -            "description": "A hostname from which the node running DTS can 
> access this node. This can also be an IP address."
> -          },
> -          "user": {
> -            "type": "string",
> -            "description": "The user to access this node with."
> -          },
> -          "password": {
> -            "type": "string",
> -            "description": "The password to use on this node. Use only as a 
> last resort. SSH keys are STRONGLY preferred."
> -          },
> -          "arch": {
> -            "$ref": "#/definitions/ARCH"
> -          },
> -          "os": {
> -            "$ref": "#/definitions/OS"
> -          },
> -          "lcores": {
> -            "type": "string",
> -            "pattern": 
> "^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$",
> -            "description": "Optional comma-separated list of logical cores 
> to use, e.g.: 1,2,3,4,5,18-22. Defaults to 1. An empty string means use all 
> lcores."
> -          },
> -          "use_first_core": {
> -            "type": "boolean",
> -            "description": "Indicate whether DPDK should use the first 
> physical core. It won't be used by default."
> -          },
> -          "memory_channels": {
> -            "type": "integer",
> -            "description": "How many memory channels to use. Optional, 
> defaults to 1."
> -          },
> -          "hugepages_2mb": {
> -            "$ref": "#/definitions/hugepages_2mb"
> -          },
> -          "ports": {
> -            "type": "array",
> -            "items": {
> -              "type": "object",
> -              "description": "Each port should be described on both sides of 
> the connection. This makes configuration slightly more verbose but greatly 
> simplifies implementation. If there are inconsistencies, then DTS will not 
> run until that issue is fixed. An example inconsistency would be port 1, node 
> 1 says it is connected to port 1, node 2, but port 1, node 2 says it is 
> connected to port 2, node 1.",
> -              "properties": {
> -                "pci": {
> -                  "$ref": "#/definitions/pci_address",
> -                  "description": "The local PCI address of the port"
> -                },
> -                "os_driver_for_dpdk": {
> -                  "type": "string",
> -                  "description": "The driver that the kernel should bind 
> this device to for DPDK to use it. (ex: vfio-pci)"
> -                },
> -                "os_driver": {
> -                  "type": "string",
> -                  "description": "The driver normally used by this port (ex: 
> i40e)"
> -                },
> -                "peer_node": {
> -                  "type": "string",
> -                  "description": "The name of the node the peer port is on"
> -                },
> -                "peer_pci": {
> -                  "$ref": "#/definitions/pci_address",
> -                  "description": "The PCI address of the peer port"
> -                }
> -              },
> -              "additionalProperties": false,
> -              "required": [
> -                "pci",
> -                "os_driver_for_dpdk",
> -                "os_driver",
> -                "peer_node",
> -                "peer_pci"
> -              ]
> -            },
> -            "minimum": 1
> -          },
> -          "traffic_generator": {
> -            "oneOf": [
> -              {
> -                "type": "object",
> -                "description": "Scapy traffic generator. Used for functional 
> testing.",
> -                "properties": {
> -                  "type": {
> -                    "type": "string",
> -                    "enum": [
> -                      "SCAPY"
> -                    ]
> -                  }
> -                }
> -              }
> -            ]
> -          }
> -        },
> -        "additionalProperties": false,
> -        "required": [
> -          "name",
> -          "hostname",
> -          "user",
> -          "arch",
> -          "os"
> -        ]
> -      },
> -      "minimum": 1
> -    },
> -    "test_runs": {
> -      "type": "array",
> -      "items": {
> -        "type": "object",
> -        "properties": {
> -          "dpdk_build": {
> -            "$ref": "#/definitions/dpdk_build"
> -          },
> -          "perf": {
> -            "type": "boolean",
> -            "description": "Enable performance testing."
> -          },
> -          "func": {
> -            "type": "boolean",
> -            "description": "Enable functional testing."
> -          },
> -          "test_suites": {
> -            "type": "array",
> -            "items": {
> -              "oneOf": [
> -                {
> -                  "$ref": "#/definitions/test_suite"
> -                },
> -                {
> -                  "$ref": "#/definitions/test_target"
> -                }
> -              ]
> -            }
> -          },
> -          "skip_smoke_tests": {
> -            "description": "Optional field that allows you to skip smoke 
> testing",
> -            "type": "boolean"
> -          },
> -          "system_under_test_node": {
> -            "type":"object",
> -            "properties": {
> -              "node_name": {
> -                "$ref": "#/definitions/node_name"
> -              },
> -              "vdevs": {
> -                "description": "Optional list of names of vdevs to be used 
> in the test run",
> -                "type": "array",
> -                "items": {
> -                  "type": "string"
> -                }
> -              }
> -            },
> -            "required": [
> -              "node_name"
> -            ]
> -          },
> -          "traffic_generator_node": {
> -            "$ref": "#/definitions/node_name"
> -          },
> -          "random_seed": {
> -            "type": "integer",
> -            "description": "Optional field. Allows you to set a seed for 
> pseudo-random generation."
> -          }
> -        },
> -        "additionalProperties": false,
> -        "required": [
> -          "dpdk_build",
> -          "perf",
> -          "func",
> -          "test_suites",
> -          "system_under_test_node",
> -          "traffic_generator_node"
> -        ]
> -      },
> -      "minimum": 1
> -    }
> -  },
> -  "required": [
> -    "test_runs",
> -    "nodes"
> -  ],
> -  "additionalProperties": false
> -}
> diff --git a/dts/framework/config/types.py b/dts/framework/config/types.py
> deleted file mode 100644
> index 02e738a61e..0000000000
> --- a/dts/framework/config/types.py
> +++ /dev/null
> @@ -1,149 +0,0 @@
> -# SPDX-License-Identifier: BSD-3-Clause
> -# Copyright(c) 2023 PANTHEON.tech s.r.o.
> -
> -"""Configuration dictionary contents specification.
> -
> -These type definitions serve as documentation of the configuration 
> dictionary contents.
> -
> -The definitions use the built-in :class:`~typing.TypedDict` construct.
> -"""
> -
> -from typing import TypedDict
> -
> -
> -class PortConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    pci: str
> -    #:
> -    os_driver_for_dpdk: str
> -    #:
> -    os_driver: str
> -    #:
> -    peer_node: str
> -    #:
> -    peer_pci: str
> -
> -
> -class TrafficGeneratorConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    type: str
> -
> -
> -class HugepageConfigurationDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    number_of: int
> -    #:
> -    force_first_numa: bool
> -
> -
> -class NodeConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    hugepages_2mb: HugepageConfigurationDict
> -    #:
> -    name: str
> -    #:
> -    hostname: str
> -    #:
> -    user: str
> -    #:
> -    password: str
> -    #:
> -    arch: str
> -    #:
> -    os: str
> -    #:
> -    lcores: str
> -    #:
> -    use_first_core: bool
> -    #:
> -    ports: list[PortConfigDict]
> -    #:
> -    memory_channels: int
> -    #:
> -    traffic_generator: TrafficGeneratorConfigDict
> -
> -
> -class DPDKBuildConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    arch: str
> -    #:
> -    os: str
> -    #:
> -    cpu: str
> -    #:
> -    compiler: str
> -    #:
> -    compiler_wrapper: str
> -
> -
> -class DPDKConfigurationDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    dpdk_tree: str | None
> -    #:
> -    tarball: str | None
> -    #:
> -    remote: bool
> -    #:
> -    precompiled_build_dir: str | None
> -    #:
> -    build_options: DPDKBuildConfigDict
> -
> -
> -class TestSuiteConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    suite: str
> -    #:
> -    cases: list[str]
> -
> -
> -class TestRunSUTConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    node_name: str
> -    #:
> -    vdevs: list[str]
> -
> -
> -class TestRunConfigDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    dpdk_build: DPDKConfigurationDict
> -    #:
> -    perf: bool
> -    #:
> -    func: bool
> -    #:
> -    skip_smoke_tests: bool
> -    #:
> -    test_suites: TestSuiteConfigDict
> -    #:
> -    system_under_test_node: TestRunSUTConfigDict
> -    #:
> -    traffic_generator_node: str
> -    #:
> -    random_seed: int
> -
> -
> -class ConfigurationDict(TypedDict):
> -    """Allowed keys and values."""
> -
> -    #:
> -    nodes: list[NodeConfigDict]
> -    #:
> -    test_runs: list[TestRunConfigDict]
> diff --git a/dts/framework/runner.py b/dts/framework/runner.py
> index 195622c653..c3d9a27a8c 100644
> --- a/dts/framework/runner.py
> +++ b/dts/framework/runner.py
> @@ -30,7 +30,15 @@
>  from framework.testbed_model.sut_node import SutNode
>  from framework.testbed_model.tg_node import TGNode
>
> -from .config import Configuration, TestRunConfiguration, TestSuiteConfig, 
> load_config
> +from .config import (
> +    Configuration,
> +    DPDKPrecompiledBuildConfiguration,
> +    SutNodeConfiguration,
> +    TestRunConfiguration,
> +    TestSuiteConfig,
> +    TGNodeConfiguration,
> +    load_config,
> +)
>  from .exception import (
>      BlockingTestSuiteError,
>      ConfigurationError,
> @@ -133,11 +141,10 @@ def run(self) -> None:
>              self._result.update_setup(Result.PASS)
>
>              # for all test run sections
> -            for test_run_config in self._configuration.test_runs:
> +            for test_run_with_nodes_config in 
> self._configuration.test_runs_with_nodes:
> +                test_run_config, sut_node_config, tg_node_config = 
> test_run_with_nodes_config
>                  self._logger.set_stage(DtsStage.test_run_setup)
> -                self._logger.info(
> -                    f"Running test run with SUT 
> '{test_run_config.system_under_test_node.name}'."
> -                )
> +                self._logger.info(f"Running test run with SUT 
> '{sut_node_config.name}'.")
>                  self._init_random_seed(test_run_config)
>                  test_run_result = self._result.add_test_run(test_run_config)
>                  # we don't want to modify the original config, so create a 
> copy
> @@ -145,7 +152,7 @@ def run(self) -> None:
>                      SETTINGS.test_suites if SETTINGS.test_suites else 
> test_run_config.test_suites
>                  )
>                  if not test_run_config.skip_smoke_tests:
> -                    test_run_test_suites[:0] = 
> [TestSuiteConfig.from_dict("smoke_tests")]
> +                    test_run_test_suites[:0] = 
> [TestSuiteConfig(test_suite="smoke_tests")]
>                  try:
>                      test_suites_with_cases = 
> self._get_test_suites_with_cases(
>                          test_run_test_suites, test_run_config.func, 
> test_run_config.perf
> @@ -161,6 +168,8 @@ def run(self) -> None:
>                      self._connect_nodes_and_run_test_run(
>                          sut_nodes,
>                          tg_nodes,
> +                        sut_node_config,
> +                        tg_node_config,
>                          test_run_config,
>                          test_run_result,
>                          test_suites_with_cases,
> @@ -223,10 +232,10 @@ def _get_test_suites_with_cases(
>          test_suites_with_cases = []
>
>          for test_suite_config in test_suite_configs:
> -            test_suite_class = 
> self._get_test_suite_class(test_suite_config.test_suite)
> +            test_suite_class = 
> self._get_test_suite_class(test_suite_config.test_suite_name)
>              test_cases: list[type[TestCase]] = []
>              func_test_cases, perf_test_cases = 
> test_suite_class.filter_test_cases(
> -                test_suite_config.test_cases
> +                test_suite_config.test_cases_names
>              )
>              if func:
>                  test_cases.extend(func_test_cases)
> @@ -305,6 +314,8 @@ def _connect_nodes_and_run_test_run(
>          self,
>          sut_nodes: dict[str, SutNode],
>          tg_nodes: dict[str, TGNode],
> +        sut_node_config: SutNodeConfiguration,
> +        tg_node_config: TGNodeConfiguration,
>          test_run_config: TestRunConfiguration,
>          test_run_result: TestRunResult,
>          test_suites_with_cases: Iterable[TestSuiteWithCases],
> @@ -319,24 +330,26 @@ def _connect_nodes_and_run_test_run(
>          Args:
>              sut_nodes: A dictionary storing connected/to be connected SUT 
> nodes.
>              tg_nodes: A dictionary storing connected/to be connected TG 
> nodes.
> +            sut_node_config: The test run's SUT node configuration.
> +            tg_node_config: The test run's TG node configuration.
>              test_run_config: A test run configuration.
>              test_run_result: The test run's result.
>              test_suites_with_cases: The test suites with test cases to run.
>          """
> -        sut_node = sut_nodes.get(test_run_config.system_under_test_node.name)
> -        tg_node = tg_nodes.get(test_run_config.traffic_generator_node.name)
> +        sut_node = sut_nodes.get(sut_node_config.name)
> +        tg_node = tg_nodes.get(tg_node_config.name)
>
>          try:
>              if not sut_node:
> -                sut_node = SutNode(test_run_config.system_under_test_node)
> +                sut_node = SutNode(sut_node_config)
>                  sut_nodes[sut_node.name] = sut_node
>              if not tg_node:
> -                tg_node = TGNode(test_run_config.traffic_generator_node)
> +                tg_node = TGNode(tg_node_config)
>                  tg_nodes[tg_node.name] = tg_node
>          except Exception as e:
> -            failed_node = test_run_config.system_under_test_node.name
> +            failed_node = test_run_config.system_under_test_node.node_name
>              if sut_node:
> -                failed_node = test_run_config.traffic_generator_node.name
> +                failed_node = test_run_config.traffic_generator_node
>              self._logger.exception(f"The Creation of node {failed_node} 
> failed.")
>              test_run_result.update_setup(Result.FAIL, e)
>
> @@ -369,14 +382,22 @@ def _run_test_run(
>              ConfigurationError: If the DPDK sources or build is not set up 
> from config or settings.
>          """
>          self._logger.info(
> -            f"Running test run with SUT 
> '{test_run_config.system_under_test_node.name}'."
> +            f"Running test run with SUT 
> '{test_run_config.system_under_test_node.node_name}'."
>          )
>          test_run_result.add_sut_info(sut_node.node_info)
>          try:
> -            dpdk_location = SETTINGS.dpdk_location or 
> test_run_config.dpdk_config.dpdk_location
> -            sut_node.set_up_test_run(test_run_config, dpdk_location)
> +            dpdk_build_config = test_run_config.dpdk_config
> +            if new_location := SETTINGS.dpdk_location:
> +                dpdk_build_config = dpdk_build_config.model_copy(
> +                    update={"dpdk_location": new_location}
> +                )
> +            if dir := SETTINGS.precompiled_build_dir:
> +                dpdk_build_config = DPDKPrecompiledBuildConfiguration(
> +                    dpdk_location=dpdk_build_config.dpdk_location, 
> precompiled_build_dir=dir
> +                )
> +            sut_node.set_up_test_run(test_run_config, dpdk_build_config)
>              
> test_run_result.add_dpdk_build_info(sut_node.get_dpdk_build_info())
> -            tg_node.set_up_test_run(test_run_config, dpdk_location)
> +            tg_node.set_up_test_run(test_run_config, dpdk_build_config)
>              test_run_result.update_setup(Result.PASS)
>          except Exception as e:
>              self._logger.exception("Test run setup failed.")
> diff --git a/dts/framework/settings.py b/dts/framework/settings.py
> index a452319b90..1253ed86ac 100644
> --- a/dts/framework/settings.py
> +++ b/dts/framework/settings.py
> @@ -60,9 +60,8 @@
>  .. option:: --precompiled-build-dir
>  .. envvar:: DTS_PRECOMPILED_BUILD_DIR
>
> -    Define the subdirectory under the DPDK tree root directory where the 
> pre-compiled binaries are
> -    located. If set, DTS will build DPDK under the `build` directory 
> instead. Can only be used with
> -    --dpdk-tree or --tarball.
> +    Define the subdirectory under the DPDK tree root directory or tarball 
> where the pre-compiled
> +    binaries are located.
>
>  .. option:: --test-suite
>  .. envvar:: DTS_TEST_SUITES
> @@ -95,13 +94,21 @@
>  import argparse
>  import os
>  import sys
> -import tarfile
>  from argparse import Action, ArgumentDefaultsHelpFormatter, _get_action_name
>  from dataclasses import dataclass, field
>  from pathlib import Path
>  from typing import Callable
>
> -from .config import DPDKLocation, TestSuiteConfig
> +from pydantic import ValidationError
> +
> +from .config import (
> +    DPDKLocation,
> +    LocalDPDKTarballLocation,
> +    LocalDPDKTreeLocation,
> +    RemoteDPDKTarballLocation,
> +    RemoteDPDKTreeLocation,
> +    TestSuiteConfig,
> +)
>
>
>  @dataclass(slots=True)
> @@ -122,6 +129,8 @@ class Settings:
>      #:
>      dpdk_location: DPDKLocation | None = None
>      #:
> +    precompiled_build_dir: str | None = None
> +    #:
>      compile_timeout: float = 1200
>      #:
>      test_suites: list[TestSuiteConfig] = field(default_factory=list)
> @@ -383,13 +392,11 @@ def _get_parser() -> _DTSArgumentParser:
>
>      action = dpdk_build.add_argument(
>          "--precompiled-build-dir",
> -        help="Define the subdirectory under the DPDK tree root directory 
> where the pre-compiled "
> -        "binaries are located. If set, DTS will build DPDK under the `build` 
> directory instead. "
> -        "Can only be used with --dpdk-tree or --tarball.",
> +        help="Define the subdirectory under the DPDK tree root directory or 
> tarball where the "
> +        "pre-compiled binaries are located.",
>          metavar="DIR_NAME",
>      )
>      _add_env_var_to_action(action)
> -    _required_with_one_of(parser, action, "dpdk_tarball_path", 
> "dpdk_tree_path")
>
>      action = parser.add_argument(
>          "--compile-timeout",
> @@ -442,61 +449,61 @@ def _get_parser() -> _DTSArgumentParser:
>
>
>  def _process_dpdk_location(
> +    parser: _DTSArgumentParser,
>      dpdk_tree: str | None,
>      tarball: str | None,
>      remote: bool,
> -    build_dir: str | None,
> -):
> +) -> DPDKLocation | None:
>      """Process and validate DPDK build arguments.
>
>      Ensures that either `dpdk_tree` or `tarball` is provided. Validate 
> existence and format of
>      `dpdk_tree` or `tarball` on local filesystem, if `remote` is False. 
> Constructs and returns
> -    the :class:`DPDKLocation` with the provided parameters if validation is 
> successful.
> +    any valid :class:`DPDKLocation` with the provided parameters if 
> validation is successful.
>
>      Args:
> -        dpdk_tree: The path to the DPDK source tree directory. Only one of 
> `dpdk_tree` or `tarball`
> -            must be provided.
> -        tarball: The path to the DPDK tarball. Only one of `dpdk_tree` or 
> `tarball` must be
> -            provided.
> +        dpdk_tree: The path to the DPDK source tree directory.
> +        tarball: The path to the DPDK tarball.
>          remote: If :data:`True`, `dpdk_tree` or `tarball` is located on the 
> SUT node, instead of the
>              execution host.
> -        build_dir: If it's defined, DPDK has been pre-built and the build 
> directory is located in a
> -            subdirectory of `dpdk_tree` or `tarball` root directory.
>
>      Returns:
>          A DPDK location if construction is successful, otherwise None.
> -
> -    Raises:
> -        argparse.ArgumentTypeError: If `dpdk_tree` or `tarball` not found in 
> local filesystem or
> -            they aren't in the right format.
>      """
> -    if not (dpdk_tree or tarball):
> -        return None
> -
> -    if not remote:
> -        if dpdk_tree:
> -            if not Path(dpdk_tree).exists():
> -                raise argparse.ArgumentTypeError(
> -                    f"DPDK tree '{dpdk_tree}' not found in local filesystem."
> -                )
> -
> -            if not Path(dpdk_tree).is_dir():
> -                raise argparse.ArgumentTypeError(f"DPDK tree '{dpdk_tree}' 
> must be a directory.")
> -
> -            dpdk_tree = os.path.realpath(dpdk_tree)
> -
> -        if tarball:
> -            if not Path(tarball).exists():
> -                raise argparse.ArgumentTypeError(
> -                    f"DPDK tarball '{tarball}' not found in local 
> filesystem."
> -                )
> -
> -            if not tarfile.is_tarfile(tarball):
> -                raise argparse.ArgumentTypeError(
> -                    f"DPDK tarball '{tarball}' must be a valid tar archive."
> -                )
> -
> -    return DPDKLocation(dpdk_tree=dpdk_tree, tarball=tarball, remote=remote, 
> build_dir=build_dir)
> +    if dpdk_tree:
> +        action = parser.find_action("dpdk_tree", _is_from_env)
> +
> +        try:
> +            if remote:
> +                return RemoteDPDKTreeLocation.model_validate({"dpdk_tree": 
> dpdk_tree})
> +            else:
> +                return LocalDPDKTreeLocation.model_validate({"dpdk_tree": 
> dpdk_tree})
> +        except ValidationError as e:
> +            print(
> +                "An error has occurred while validating the DPDK tree 
> supplied in the "
> +                f"{'environment variable' if action else 'arguments'}:",
> +                file=sys.stderr,
> +            )
> +            print(e, file=sys.stderr)
> +            sys.exit(1)
> +
> +    if tarball:
> +        action = parser.find_action("tarball", _is_from_env)
> +
> +        try:
> +            if remote:
> +                return RemoteDPDKTarballLocation.model_validate({"tarball": 
> tarball})
> +            else:
> +                return LocalDPDKTarballLocation.model_validate({"tarball": 
> tarball})
> +        except ValidationError as e:
> +            print(
> +                "An error has occurred while validating the DPDK tarball 
> supplied in the "
> +                f"{'environment variable' if action else 'arguments'}:",
> +                file=sys.stderr,
> +            )
> +            print(e, file=sys.stderr)
> +            sys.exit(1)
> +
> +    return None
>
>
>  def _process_test_suites(
> @@ -512,11 +519,24 @@ def _process_test_suites(
>      Returns:
>          A list of test suite configurations to execute.
>      """
> -    if parser.find_action("test_suites", _is_from_env):
> +    action = parser.find_action("test_suites", _is_from_env)
> +    if action:
>          # Environment variable in the form of "SUITE1 CASE1 CASE2, SUITE2 
> CASE1, SUITE3, ..."
>          args = [suite_with_cases.split() for suite_with_cases in 
> args[0][0].split(",")]
>
> -    return [TestSuiteConfig(test_suite, test_cases) for [test_suite, 
> *test_cases] in args]
> +    try:
> +        return [
> +            TestSuiteConfig(test_suite=test_suite, test_cases=test_cases)
> +            for [test_suite, *test_cases] in args
> +        ]
> +    except ValidationError as e:
> +        print(
> +            "An error has occurred while validating the test suites supplied 
> in the "
> +            f"{'environment variable' if action else 'arguments'}:",
> +            file=sys.stderr,
> +        )
> +        print(e, file=sys.stderr)
> +        sys.exit(1)
>
>
>  def get_settings() -> Settings:
> @@ -536,7 +556,7 @@ def get_settings() -> Settings:
>      args = parser.parse_args()
>
>      args.dpdk_location = _process_dpdk_location(
> -        args.dpdk_tree_path, args.dpdk_tarball_path, args.remote_source, 
> args.precompiled_build_dir
> +        parser, args.dpdk_tree_path, args.dpdk_tarball_path, 
> args.remote_source
>      )
>      args.test_suites = _process_test_suites(parser, args.test_suites)
>
> diff --git a/dts/framework/testbed_model/node.py 
> b/dts/framework/testbed_model/node.py
> index 62867fd80c..6031eaf937 100644
> --- a/dts/framework/testbed_model/node.py
> +++ b/dts/framework/testbed_model/node.py
> @@ -17,7 +17,12 @@
>  from ipaddress import IPv4Interface, IPv6Interface
>  from typing import Union
>
> -from framework.config import OS, DPDKLocation, NodeConfiguration, 
> TestRunConfiguration
> +from framework.config import (
> +    OS,
> +    DPDKBuildConfiguration,
> +    NodeConfiguration,
> +    TestRunConfiguration,
> +)
>  from framework.exception import ConfigurationError
>  from framework.logger import DTSLogger, get_dts_logger
>
> @@ -89,13 +94,15 @@ def __init__(self, node_config: NodeConfiguration):
>          self._init_ports()
>
>      def _init_ports(self) -> None:
> -        self.ports = [Port(port_config) for port_config in self.config.ports]
> +        self.ports = [Port(self.name, port_config) for port_config in 
> self.config.ports]
>          self.main_session.update_ports(self.ports)
>          for port in self.ports:
>              self.configure_port_state(port)
>
>      def set_up_test_run(
> -        self, test_run_config: TestRunConfiguration, dpdk_location: 
> DPDKLocation
> +        self,
> +        test_run_config: TestRunConfiguration,
> +        dpdk_build_config: DPDKBuildConfiguration,
>      ) -> None:
>          """Test run setup steps.
>
> @@ -105,7 +112,7 @@ def set_up_test_run(
>          Args:
>              test_run_config: A test run configuration according to which
>                  the setup steps will be taken.
> -            dpdk_location: The target source of the DPDK tree.
> +            dpdk_build_config: The build configuration of DPDK.
>          """
>          self._setup_hugepages()
>
> diff --git a/dts/framework/testbed_model/os_session.py 
> b/dts/framework/testbed_model/os_session.py
> index 5f087f40d6..42ab4bb8fd 100644
> --- a/dts/framework/testbed_model/os_session.py
> +++ b/dts/framework/testbed_model/os_session.py
> @@ -364,7 +364,7 @@ def extract_remote_tarball(
>          """
>
>      @abstractmethod
> -    def is_remote_dir(self, remote_path: str) -> bool:
> +    def is_remote_dir(self, remote_path: PurePath) -> bool:
>          """Check if the `remote_path` is a directory.
>
>          Args:
> @@ -375,7 +375,7 @@ def is_remote_dir(self, remote_path: str) -> bool:
>          """
>
>      @abstractmethod
> -    def is_remote_tarfile(self, remote_tarball_path: str) -> bool:
> +    def is_remote_tarfile(self, remote_tarball_path: PurePath) -> bool:
>          """Check if the `remote_tarball_path` is a tar archive.
>
>          Args:
> diff --git a/dts/framework/testbed_model/port.py 
> b/dts/framework/testbed_model/port.py
> index 82c84cf4f8..817405bea4 100644
> --- a/dts/framework/testbed_model/port.py
> +++ b/dts/framework/testbed_model/port.py
> @@ -54,7 +54,7 @@ class Port:
>      mac_address: str = ""
>      logical_name: str = ""
>
> -    def __init__(self, config: PortConfig):
> +    def __init__(self, node_name: str, config: PortConfig):
>          """Initialize the port from `node_name` and `config`.
>
>          Args:
> @@ -62,7 +62,7 @@ def __init__(self, config: PortConfig):
>              config: The test run configuration of the port.
>          """
>          self.identifier = PortIdentifier(
> -            node=config.node,
> +            node=node_name,
>              pci=config.pci,
>          )
>          self.os_driver = config.os_driver
> diff --git a/dts/framework/testbed_model/posix_session.py 
> b/dts/framework/testbed_model/posix_session.py
> index 0d3abbc519..6b66f33e22 100644
> --- a/dts/framework/testbed_model/posix_session.py
> +++ b/dts/framework/testbed_model/posix_session.py
> @@ -201,12 +201,12 @@ def extract_remote_tarball(
>          if expected_dir:
>              self.send_command(f"ls {expected_dir}", verify=True)
>
> -    def is_remote_dir(self, remote_path: str) -> bool:
> +    def is_remote_dir(self, remote_path: PurePath) -> bool:
>          """Overrides :meth:`~.os_session.OSSession.is_remote_dir`."""
>          result = self.send_command(f"test -d {remote_path}")
>          return not result.return_code
>
> -    def is_remote_tarfile(self, remote_tarball_path: str) -> bool:
> +    def is_remote_tarfile(self, remote_tarball_path: PurePath) -> bool:
>          """Overrides :meth:`~.os_session.OSSession.is_remote_tarfile`."""
>          result = self.send_command(f"tar -tvf {remote_tarball_path}")
>          return not result.return_code
> @@ -393,4 +393,8 @@ def get_node_info(self) -> NodeInfo:
>              SETTINGS.timeout,
>          ).stdout.split("\n")
>          kernel_version = self.send_command("uname -r", 
> SETTINGS.timeout).stdout
> -        return NodeInfo(os_release_info[0].strip(), 
> os_release_info[1].strip(), kernel_version)
> +        return NodeInfo(
> +            os_name=os_release_info[0].strip(),
> +            os_version=os_release_info[1].strip(),
> +            kernel_version=kernel_version,
> +        )
> diff --git a/dts/framework/testbed_model/sut_node.py 
> b/dts/framework/testbed_model/sut_node.py
> index a6c42b548c..57337c8e7d 100644
> --- a/dts/framework/testbed_model/sut_node.py
> +++ b/dts/framework/testbed_model/sut_node.py
> @@ -15,11 +15,17 @@
>  import os
>  import time
>  from dataclasses import dataclass
> -from pathlib import PurePath
> +from pathlib import Path, PurePath
>
>  from framework.config import (
>      DPDKBuildConfiguration,
> -    DPDKLocation,
> +    DPDKBuildOptionsConfiguration,
> +    DPDKPrecompiledBuildConfiguration,
> +    DPDKUncompiledBuildConfiguration,
> +    LocalDPDKTarballLocation,
> +    LocalDPDKTreeLocation,
> +    RemoteDPDKTarballLocation,
> +    RemoteDPDKTreeLocation,
>      SutNodeConfiguration,
>      TestRunConfiguration,
>  )
> @@ -178,7 +184,9 @@ def get_dpdk_build_info(self) -> DPDKBuildInfo:
>          return DPDKBuildInfo(dpdk_version=self.dpdk_version, 
> compiler_version=self.compiler_version)
>
>      def set_up_test_run(
> -        self, test_run_config: TestRunConfiguration, dpdk_location: 
> DPDKLocation
> +        self,
> +        test_run_config: TestRunConfiguration,
> +        dpdk_build_config: DPDKBuildConfiguration,
>      ) -> None:
>          """Extend the test run setup with vdev config and DPDK build set up.
>
> @@ -188,12 +196,12 @@ def set_up_test_run(
>          Args:
>              test_run_config: A test run configuration according to which
>                  the setup steps will be taken.
> -            dpdk_location: The target source of the DPDK tree.
> +            dpdk_build_config: The build configuration of DPDK.
>          """
> -        super().set_up_test_run(test_run_config, dpdk_location)
> -        for vdev in test_run_config.vdevs:
> +        super().set_up_test_run(test_run_config, dpdk_build_config)
> +        for vdev in test_run_config.system_under_test_node.vdevs:
>              self.virtual_devices.append(VirtualDevice(vdev))
> -        self._set_up_dpdk(dpdk_location, 
> test_run_config.dpdk_config.dpdk_build_config)
> +        self._set_up_dpdk(dpdk_build_config)
>
>      def tear_down_test_run(self) -> None:
>          """Extend the test run teardown with virtual device teardown and 
> DPDK teardown."""
> @@ -202,7 +210,8 @@ def tear_down_test_run(self) -> None:
>          self._tear_down_dpdk()
>
>      def _set_up_dpdk(
> -        self, dpdk_location: DPDKLocation, dpdk_build_config: 
> DPDKBuildConfiguration | None
> +        self,
> +        dpdk_build_config: DPDKBuildConfiguration,
>      ) -> None:
>          """Set up DPDK the SUT node and bind ports.
>
> @@ -211,21 +220,26 @@ def _set_up_dpdk(
>          are bound to those that DPDK needs.
>
>          Args:
> -            dpdk_location: The location of the DPDK tree.
> -            dpdk_build_config: A DPDK build configuration to test. If 
> :data:`None`,
> -                DTS will use pre-built DPDK from a :dataclass:`DPDKLocation`.
> +            dpdk_build_config: A DPDK build configuration to test.
>          """
> -        self._set_remote_dpdk_tree_path(dpdk_location.dpdk_tree, 
> dpdk_location.remote)
> -        if not self._remote_dpdk_tree_path:
> -            if dpdk_location.dpdk_tree:
> -                self._copy_dpdk_tree(dpdk_location.dpdk_tree)
> -            elif dpdk_location.tarball:
> -                
> self._prepare_and_extract_dpdk_tarball(dpdk_location.tarball, 
> dpdk_location.remote)
> -
> -        self._set_remote_dpdk_build_dir(dpdk_location.build_dir)
> -        if not self.remote_dpdk_build_dir and dpdk_build_config:
> -            self._configure_dpdk_build(dpdk_build_config)
> -            self._build_dpdk()
> +        match dpdk_build_config.dpdk_location:
> +            case RemoteDPDKTreeLocation(dpdk_tree=dpdk_tree):
> +                self._set_remote_dpdk_tree_path(dpdk_tree)
> +            case LocalDPDKTreeLocation(dpdk_tree=dpdk_tree):
> +                self._copy_dpdk_tree(dpdk_tree)
> +            case RemoteDPDKTarballLocation(tarball=tarball):
> +                self._validate_remote_dpdk_tarball(tarball)
> +                self._prepare_and_extract_dpdk_tarball(tarball)
> +            case LocalDPDKTarballLocation(tarball=tarball):
> +                remote_tarball = self._copy_dpdk_tarball_to_remote(tarball)
> +                self._prepare_and_extract_dpdk_tarball(remote_tarball)
> +
> +        match dpdk_build_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()
>
> @@ -238,37 +252,29 @@ def _tear_down_dpdk(self) -> None:
>          self.compiler_version = None
>          self.bind_ports_to_driver(for_dpdk=False)
>
> -    def _set_remote_dpdk_tree_path(self, dpdk_tree: str | None, remote: 
> bool):
> +    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.
>
> -        If :data:`dpdk_tree` and :data:`remote` are defined, check existence 
> of :data:`dpdk_tree`
> -        on SUT node and sets the `_remote_dpdk_tree_path` property. 
> Otherwise, sets nothing.
> -
>          Verify DPDK source tree existence on the SUT node, if exists sets the
>          `_remote_dpdk_tree_path` property, otherwise sets nothing.
>
>          Args:
>              dpdk_tree: The path to the DPDK source tree directory.
> -            remote: Indicates whether the `dpdk_tree` is already on the SUT 
> node, instead of the
> -                execution host.
>
>          Raises:
>              RemoteFileNotFoundError: If the DPDK source tree is expected to 
> be on the SUT node but
>                  is not found.
>          """
> -        if remote and dpdk_tree:
> -            if not self.main_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):
> -                raise ConfigurationError(
> -                    f"Remote DPDK source tree '{dpdk_tree}' must be a 
> directory."
> -                )
> -
> -            self.__remote_dpdk_tree_path = PurePath(dpdk_tree)
> -
> -    def _copy_dpdk_tree(self, dpdk_tree_path: str) -> None:
> +        if not self.main_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):
> +            raise ConfigurationError(f"Remote DPDK source tree '{dpdk_tree}' 
> must be a directory.")
> +
> +        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.
>
>          Args:
> @@ -288,25 +294,45 @@ def _copy_dpdk_tree(self, dpdk_tree_path: str) -> None:
>              self._remote_tmp_dir, PurePath(dpdk_tree_path).name
>          )
>
> -    def _prepare_and_extract_dpdk_tarball(self, dpdk_tarball: str, remote: 
> bool) -> None:
> -        """Ensure the DPDK tarball is available on the SUT node and extract 
> it.
> +    def _validate_remote_dpdk_tarball(self, dpdk_tarball: PurePath) -> None:
> +        """Validate the DPDK tarball on the SUT node.
>
> -        This method ensures that the DPDK source tree tarball is available 
> on the
> -        SUT node. If the `dpdk_tarball` is local, it is copied to the SUT 
> node. If the
> -        `dpdk_tarball` is already on the SUT node, it verifies its existence.
> -        The `dpdk_tarball` is then extracted on the SUT node.
> +        Args:
> +            dpdk_tarball: The path to the DPDK tarball on the SUT node.
>
> -        This method sets the `_remote_dpdk_tree_path` property to the path 
> of the
> -        extracted DPDK tree on the SUT node.
> +        Raises:
> +            RemoteFileNotFoundError: If the `dpdk_tarball` is expected to be 
> on the SUT node but is
> +                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):
> +            raise RemoteFileNotFoundError(f"Remote DPDK tarball 
> '{dpdk_tarball}' not found in SUT.")
> +        if not self.main_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:
> +        """Copy the local DPDK tarball to the SUT node.
>
>          Args:
> -            dpdk_tarball: The path to the DPDK tarball, either locally or on 
> the SUT node.
> -            remote: Indicates whether the `dpdk_tarball` is already on the 
> SUT node, instead of the
> -                execution host.
> +            dpdk_tarball: The local path to the DPDK tarball.
>
> -        Raises:
> -            RemoteFileNotFoundError: If the `dpdk_tarball` is expected to be 
> on the SUT node but
> -                is not found.
> +        Returns:
> +            The path of the copied tarball on the SUT node.
> +        """
> +        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)
> +
> +    def _prepare_and_extract_dpdk_tarball(self, remote_tarball_path: 
> PurePath) -> None:
> +        """Prepare the remote DPDK tree path and extract the tarball.
> +
> +        This method extracts the remote tarball and sets the 
> `_remote_dpdk_tree_path` property to
> +        the path of the extracted DPDK tree on the SUT node.
> +
> +        Args:
> +            remote_tarball_path: The path to the DPDK tarball on the SUT 
> node.
>          """
>
>          def remove_tarball_suffix(remote_tarball_path: PurePath) -> PurePath:
> @@ -324,30 +350,9 @@ 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("")
>
> -        if remote:
> -            if not self.main_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):
> -                raise ConfigurationError(
> -                    f"Remote DPDK tarball '{dpdk_tarball}' must be a tar 
> archive."
> -                )
> -
> -            remote_tarball_path = PurePath(dpdk_tarball)
> -        else:
> -            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)
> -
> -            remote_tarball_path = self.main_session.join_remote_path(
> -                self._remote_tmp_dir, PurePath(dpdk_tarball).name
> -            )
> -
>          tarball_top_dir = 
> self.main_session.get_tarball_top_dir(remote_tarball_path)
>          self.__remote_dpdk_tree_path = self.main_session.join_remote_path(
> -            PurePath(remote_tarball_path).parent,
> +            remote_tarball_path.parent,
>              tarball_top_dir or remove_tarball_suffix(remote_tarball_path),
>          )
>
> @@ -360,33 +365,32 @@ def remove_tarball_suffix(remote_tarball_path: 
> PurePath) -> PurePath:
>              self._remote_dpdk_tree_path,
>          )
>
> -    def _set_remote_dpdk_build_dir(self, build_dir: str | None):
> +    def _set_remote_dpdk_build_dir(self, build_dir: str):
>          """Set the `remote_dpdk_build_dir` on the SUT.
>
> -        If :data:`build_dir` is defined, check existence on the SUT node and 
> sets the
> +        Check existence on the SUT node and sets the
>          `remote_dpdk_build_dir` property by joining the 
> `_remote_dpdk_tree_path` and `build_dir`.
>          Otherwise, sets nothing.
>
>          Args:
> -            build_dir: If it's defined, DPDK has been pre-built and the 
> build directory is located
> +            build_dir: DPDK has been pre-built and the build directory is 
> located
>                  in a subdirectory of `dpdk_tree` or `tarball` root directory.
>
>          Raises:
>              RemoteFileNotFoundError: If the `build_dir` is expected but does 
> not exist on the SUT
>                  node.
>          """
> -        if build_dir:
> -            remote_dpdk_build_dir = self.main_session.join_remote_path(
> -                self._remote_dpdk_tree_path, build_dir
> +        remote_dpdk_build_dir = self.main_session.join_remote_path(
> +            self._remote_dpdk_tree_path, build_dir
> +        )
> +        if not self.main_session.remote_path_exists(remote_dpdk_build_dir):
> +            raise RemoteFileNotFoundError(
> +                f"Remote DPDK build dir '{remote_dpdk_build_dir}' not found 
> in SUT node."
>              )
> -            if not 
> self.main_session.remote_path_exists(remote_dpdk_build_dir):
> -                raise RemoteFileNotFoundError(
> -                    f"Remote DPDK build dir '{remote_dpdk_build_dir}' not 
> found in SUT node."
> -                )
>
> -            self._remote_dpdk_build_dir = PurePath(remote_dpdk_build_dir)
> +        self._remote_dpdk_build_dir = PurePath(remote_dpdk_build_dir)
>
> -    def _configure_dpdk_build(self, dpdk_build_config: 
> DPDKBuildConfiguration) -> None:
> +    def _configure_dpdk_build(self, dpdk_build_config: 
> DPDKBuildOptionsConfiguration) -> None:
>          """Populate common environment variables and set the DPDK build 
> related properties.
>
>          This method sets `compiler_version` for additional information and 
> `remote_dpdk_build_dir`
> diff --git a/dts/framework/testbed_model/topology.py 
> b/dts/framework/testbed_model/topology.py
> index d38ae36c2a..17b333e76a 100644
> --- a/dts/framework/testbed_model/topology.py
> +++ b/dts/framework/testbed_model/topology.py
> @@ -99,7 +99,16 @@ def __init__(self, sut_ports: Iterable[Port], tg_ports: 
> Iterable[Port]):
>                      port_links.append(PortLink(sut_port=sut_port, 
> tg_port=tg_port))
>
>          self.type = TopologyType.get_from_value(len(port_links))
> -        dummy_port = Port(PortConfig("", "", "", "", "", ""))
> +        dummy_port = Port(
> +            "",
> +            PortConfig(
> +                pci="0000:00:00.0",
> +                os_driver_for_dpdk="",
> +                os_driver="",
> +                peer_node="",
> +                peer_pci="0000:00:00.0",
> +            ),
> +        )
>          self.tg_port_egress = dummy_port
>          self.sut_port_ingress = dummy_port
>          self.sut_port_egress = dummy_port
> diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py 
> b/dts/framework/testbed_model/traffic_generator/__init__.py
> index a319fa5320..945f6bbbbb 100644
> --- a/dts/framework/testbed_model/traffic_generator/__init__.py
> +++ b/dts/framework/testbed_model/traffic_generator/__init__.py
> @@ -38,6 +38,4 @@ def create_traffic_generator(
>          case ScapyTrafficGeneratorConfig():
>              return ScapyTrafficGenerator(tg_node, traffic_generator_config, 
> privileged=True)
>          case _:
> -            raise ConfigurationError(
> -                f"Unknown traffic generator: 
> {traffic_generator_config.traffic_generator_type}"
> -            )
> +            raise ConfigurationError(f"Unknown traffic generator: 
> {traffic_generator_config.type}")
> diff --git 
> a/dts/framework/testbed_model/traffic_generator/traffic_generator.py 
> b/dts/framework/testbed_model/traffic_generator/traffic_generator.py
> index 469a12a780..5ac61cd4e1 100644
> --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py
> +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py
> @@ -45,7 +45,7 @@ def __init__(self, tg_node: Node, config: 
> TrafficGeneratorConfig, **kwargs):
>          """
>          self._config = config
>          self._tg_node = tg_node
> -        self._logger = get_dts_logger(f"{self._tg_node.name} 
> {self._config.traffic_generator_type}")
> +        self._logger = get_dts_logger(f"{self._tg_node.name} 
> {self._config.type}")
>          super().__init__(tg_node, **kwargs)
>
>      def send_packet(self, packet: Packet, port: Port) -> None:
> diff --git a/dts/framework/utils.py b/dts/framework/utils.py
> index 78a39e32c7..e862e3ac66 100644
> --- a/dts/framework/utils.py
> +++ b/dts/framework/utils.py
> @@ -28,7 +28,7 @@
>
>  from .exception import InternalError
>
> -REGEX_FOR_PCI_ADDRESS: str = 
> "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"
> +REGEX_FOR_PCI_ADDRESS: str = 
> r"[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}"
>  _REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC: str = 
> r"(?:[\da-fA-F]{2}[:-]){5}[\da-fA-F]{2}"
>  _REGEX_FOR_DOT_SEP_MAC: str = r"(?:[\da-fA-F]{4}.){2}[\da-fA-F]{4}"
>  REGEX_FOR_MAC_ADDRESS: str = 
> rf"{_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC}|{_REGEX_FOR_DOT_SEP_MAC}"
> diff --git a/dts/tests/TestSuite_smoke_tests.py 
> b/dts/tests/TestSuite_smoke_tests.py
> index d7870bd40f..bc3a2a6bf9 100644
> --- a/dts/tests/TestSuite_smoke_tests.py
> +++ b/dts/tests/TestSuite_smoke_tests.py
> @@ -127,7 +127,7 @@ def test_device_bound_to_driver(self) -> None:
>          path_to_devbind = self.sut_node.path_to_devbind_script
>
>          all_nics_in_dpdk_devbind = self.sut_node.main_session.send_command(
> -            f"{path_to_devbind} --status | awk '{REGEX_FOR_PCI_ADDRESS}'",
> +            f"{path_to_devbind} --status | awk '/{REGEX_FOR_PCI_ADDRESS}/'",
>              SETTINGS.timeout,
>          ).stdout
>
> --
> 2.43.0
>

Reply via email to