From patchwork Wed Jan 15 14:18:03 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150105 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 5ED084608B; Wed, 15 Jan 2025 15:19:39 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 37E00402E7; Wed, 15 Jan 2025 15:19:36 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id B56584003C for ; Wed, 15 Jan 2025 15:19:33 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 5D35E12FC; Wed, 15 Jan 2025 06:20:01 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 3C7C63F63F; Wed, 15 Jan 2025 06:19:32 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 1/7] dts: enable arch self-discovery Date: Wed, 15 Jan 2025 14:18:03 +0000 Message-ID: <20250115141809.3898708-2-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org From: Nicholas Pratte The 'arch' attribute in the conf.yaml is unnecessary, as this can be readily discovered directly from any given node. Bugzilla ID: 1360 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx Reviewed-by: Nicholas Pratte --- dts/conf.yaml | 2 -- dts/framework/config/__init__.py | 2 -- dts/framework/testbed_model/node.py | 3 +++ dts/framework/testbed_model/os_session.py | 8 ++++++++ dts/framework/testbed_model/posix_session.py | 4 ++++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/dts/conf.yaml b/dts/conf.yaml index f83dbb0e90..80aba0d63a 100644 --- a/dts/conf.yaml +++ b/dts/conf.yaml @@ -42,7 +42,6 @@ nodes: - name: "SUT 1" hostname: sut1.change.me.localhost user: dtsuser - arch: x86_64 os: linux lcores: "" # use all the available logical cores use_first_core: false # tells DPDK to use any physical core @@ -68,7 +67,6 @@ nodes: - name: "TG 1" hostname: tg1.change.me.localhost user: dtsuser - arch: x86_64 os: linux ports: # sets up the physical link between "TG 1"@0000:00:08.0 and "SUT 1"@0000:00:08.0 diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 6bf4885815..1127c6474a 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -191,8 +191,6 @@ class NodeConfiguration(FrozenModel): user: str #: The password of the user. The use of passwords is heavily discouraged, please use SSH keys. password: str | None = None - #: The architecture of the :class:`~framework.testbed_model.node.Node`. - arch: Architecture #: The operating system of the :class:`~framework.testbed_model.node.Node`. os: OS #: A comma delimited list of logical cores to use when running DPDK. diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index c6f12319ca..c56872aa99 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -17,6 +17,7 @@ from framework.config import ( OS, + Architecture, DPDKBuildConfiguration, NodeConfiguration, TestRunConfiguration, @@ -57,6 +58,7 @@ class Node(ABC): main_session: OSSession config: NodeConfiguration name: str + arch: Architecture lcores: list[LogicalCore] ports: list[Port] _logger: DTSLogger @@ -79,6 +81,7 @@ def __init__(self, node_config: NodeConfiguration): self.name = node_config.name self._logger = get_dts_logger(self.name) self.main_session = create_session(self.config, self.name, self._logger) + self.arch = Architecture(self.main_session.get_arch_info()) self._logger.info(f"Connected to node: {self.name}") diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py index 28eccc05ed..30d781c355 100644 --- a/dts/framework/testbed_model/os_session.py +++ b/dts/framework/testbed_model/os_session.py @@ -507,6 +507,14 @@ def get_node_info(self) -> OSSessionInfo: Node information. """ + @abstractmethod + def get_arch_info(self) -> str: + """Discover CPU architecture of the remote host. + + Returns: + Remote host CPU architecture. + """ + @abstractmethod def update_ports(self, ports: list[Port]) -> None: """Get additional information about ports from the operating system and update them. diff --git a/dts/framework/testbed_model/posix_session.py b/dts/framework/testbed_model/posix_session.py index 29e314db6e..220618cacc 100644 --- a/dts/framework/testbed_model/posix_session.py +++ b/dts/framework/testbed_model/posix_session.py @@ -404,3 +404,7 @@ def get_node_info(self) -> OSSessionInfo: ).stdout.split("\n") kernel_version = self.send_command("uname -r", SETTINGS.timeout).stdout return OSSessionInfo(os_release_info[0].strip(), os_release_info[1].strip(), kernel_version) + + def get_arch_info(self) -> str: + """Overrides :meth'~.os_session.OSSession.get_arch_info'.""" + return self.send_command("uname -m").stdout.strip() From patchwork Wed Jan 15 14:18:04 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150106 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 03DC24608B; Wed, 15 Jan 2025 15:19:46 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 00CEC402ED; Wed, 15 Jan 2025 15:19:38 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id B824C402B5 for ; Wed, 15 Jan 2025 15:19:34 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 8C9AA11FB; Wed, 15 Jan 2025 06:20:02 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 615BD3F63F; Wed, 15 Jan 2025 06:19:33 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 2/7] dts: simplify build options config Date: Wed, 15 Jan 2025 14:18:04 +0000 Message-ID: <20250115141809.3898708-3-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org From: Nicholas Pratte The build options configuration contained redundant fields that were not in use, and there is no future scope for their use. Bugzilla ID: 1360 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx Reviewed-by: Nicholas Pratte --- dts/conf.yaml | 3 -- dts/framework/config/__init__.py | 43 -------------------- dts/framework/test_result.py | 2 +- dts/framework/testbed_model/cpu.py | 20 ++++++++- dts/framework/testbed_model/node.py | 2 +- dts/framework/testbed_model/os_session.py | 4 +- dts/framework/testbed_model/posix_session.py | 2 +- dts/framework/testbed_model/sut_node.py | 6 ++- 8 files changed, 28 insertions(+), 54 deletions(-) diff --git a/dts/conf.yaml b/dts/conf.yaml index 80aba0d63a..4b6965b3d7 100644 --- a/dts/conf.yaml +++ b/dts/conf.yaml @@ -14,9 +14,6 @@ test_runs: # precompiled_build_dir: Commented out because `build_options` is defined. build_options: - arch: x86_64 - os: linux - cpu: native # the combination of the following two makes CC="ccache gcc" compiler: gcc compiler_wrapper: ccache # Optional. diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 1127c6474a..3fa8f4fa8f 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -63,22 +63,6 @@ class FrozenModel(BaseModel): model_config = ConfigDict(frozen=True, extra="forbid") -@unique -class Architecture(StrEnum): - r"""The supported architectures of :class:`~framework.testbed_model.node.Node`\s.""" - - #: - i686 = auto() - #: - x86_64 = auto() - #: - x86_32 = auto() - #: - arm64 = auto() - #: - ppc64le = auto() - - @unique class OS(StrEnum): r"""The supported operating systems of :class:`~framework.testbed_model.node.Node`\s.""" @@ -91,22 +75,6 @@ class OS(StrEnum): windows = auto() -@unique -class CPUType(StrEnum): - r"""The supported CPUs of :class:`~framework.testbed_model.node.Node`\s.""" - - #: - native = auto() - #: - armv8a = auto() - #: - dpaa2 = auto() - #: - thunderx = auto() - #: - xgene1 = auto() - - @unique class Compiler(StrEnum): r"""The supported compilers of :class:`~framework.testbed_model.node.Node`\s.""" @@ -351,23 +319,12 @@ class DPDKBuildOptionsConfiguration(FrozenModel): The build options used for building DPDK. """ - #: The target architecture to build for. - arch: Architecture - #: The target OS to build for. - os: OS - #: The target CPU to build for. - cpu: CPUType #: The compiler executable to use. compiler: Compiler #: This string will be put in front of the compiler when executing the build. Useful for adding #: wrapper commands, such as ``ccache``. compiler_wrapper: str = "" - @cached_property - def name(self) -> str: - """The name of the compiler.""" - return f"{self.arch}-{self.os}-{self.cpu}-{self.compiler}" - class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration): """DPDK uncompiled build configuration.""" diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py index ba7c1c9804..381f72b974 100644 --- a/dts/framework/test_result.py +++ b/dts/framework/test_result.py @@ -337,7 +337,7 @@ class DTSResult(BaseResult): """Stores environment information and test results from a DTS run. * Test run level information, such as testbed, the test suite list and - DPDK build configuration (compiler, target OS and cpu), + DPDK build compiler configuration, * Test suite and test case results, * All errors that are caught and recorded during DTS execution. diff --git a/dts/framework/testbed_model/cpu.py b/dts/framework/testbed_model/cpu.py index 46bf13960d..d19fa5d597 100644 --- a/dts/framework/testbed_model/cpu.py +++ b/dts/framework/testbed_model/cpu.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright(c) 2023 PANTHEON.tech s.r.o. +# Copyright(c) 2025 Arm Limited """CPU core representation and filtering. @@ -21,8 +22,25 @@ from abc import ABC, abstractmethod from collections.abc import Iterable, ValuesView from dataclasses import dataclass +from enum import auto, unique -from framework.utils import expand_range +from framework.utils import StrEnum, expand_range + + +@unique +class Architecture(StrEnum): + r"""The supported architectures of :class:`~framework.testbed_model.node.Node`\s.""" + + #: + i686 = auto() + #: + x86_64 = auto() + #: + x86_32 = auto() + #: + aarch64 = auto() + #: + ppc64le = auto() @dataclass(slots=True, frozen=True) diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index c56872aa99..08328ee482 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -17,7 +17,6 @@ from framework.config import ( OS, - Architecture, DPDKBuildConfiguration, NodeConfiguration, TestRunConfiguration, @@ -26,6 +25,7 @@ from framework.logger import DTSLogger, get_dts_logger from .cpu import ( + Architecture, LogicalCore, LogicalCoreCount, LogicalCoreList, diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py index 30d781c355..fcda9b3de1 100644 --- a/dts/framework/testbed_model/os_session.py +++ b/dts/framework/testbed_model/os_session.py @@ -28,7 +28,7 @@ from dataclasses import dataclass from pathlib import Path, PurePath, PurePosixPath -from framework.config import Architecture, NodeConfiguration +from framework.config import NodeConfiguration from framework.logger import DTSLogger from framework.remote_session import ( InteractiveRemoteSession, @@ -40,7 +40,7 @@ from framework.settings import SETTINGS from framework.utils import MesonArgs, TarCompressionFormat -from .cpu import LogicalCore +from .cpu import Architecture, LogicalCore from .port import Port diff --git a/dts/framework/testbed_model/posix_session.py b/dts/framework/testbed_model/posix_session.py index 220618cacc..981600e24c 100644 --- a/dts/framework/testbed_model/posix_session.py +++ b/dts/framework/testbed_model/posix_session.py @@ -15,7 +15,6 @@ from collections.abc import Iterable from pathlib import Path, PurePath, PurePosixPath -from framework.config import Architecture from framework.exception import DPDKBuildError, RemoteCommandExecutionError from framework.settings import SETTINGS from framework.utils import ( @@ -26,6 +25,7 @@ extract_tarball, ) +from .cpu import Architecture from .os_session import OSSession, OSSessionInfo diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py index a9dc0a474a..11d4b22089 100644 --- a/dts/framework/testbed_model/sut_node.py +++ b/dts/framework/testbed_model/sut_node.py @@ -400,7 +400,7 @@ def _configure_dpdk_build(self, dpdk_build_config: DPDKBuildOptionsConfiguration dpdk_build_config: A DPDK build configuration to test. """ self._env_vars = {} - self._env_vars.update(self.main_session.get_dpdk_build_env_vars(dpdk_build_config.arch)) + self._env_vars.update(self.main_session.get_dpdk_build_env_vars(self.arch)) if compiler_wrapper := dpdk_build_config.compiler_wrapper: self._env_vars["CC"] = f"'{compiler_wrapper} {dpdk_build_config.compiler.name}'" else: @@ -410,8 +410,10 @@ def _configure_dpdk_build(self, dpdk_build_config: DPDKBuildOptionsConfiguration dpdk_build_config.compiler.name ) + build_dir_name = f"{self.arch}-{self.config.os}-{dpdk_build_config.compiler}" + self._remote_dpdk_build_dir = self.main_session.join_remote_path( - self._remote_dpdk_tree_path, dpdk_build_config.name + self._remote_dpdk_tree_path, build_dir_name ) def _build_dpdk(self) -> None: From patchwork Wed Jan 15 14:18:05 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150107 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id CFC974608B; Wed, 15 Jan 2025 15:19:52 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 3C418402F2; Wed, 15 Jan 2025 15:19:39 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id CAFB8402DD for ; Wed, 15 Jan 2025 15:19:35 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id B11B112FC; Wed, 15 Jan 2025 06:20:03 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 90F813F63F; Wed, 15 Jan 2025 06:19:34 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 3/7] dts: infer use first core without config Date: Wed, 15 Jan 2025 14:18:05 +0000 Message-ID: <20250115141809.3898708-4-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org From: Nicholas Pratte To further the simplification of the user configuration, use_first_core can be inferred from the lcores. If the user explicitly includes the core 0 in the lcores range, it will only then be used. Bugzilla ID: 1360 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx Reviewed-by: Nicholas Pratte --- dts/conf.yaml | 3 +-- dts/framework/config/__init__.py | 19 ++++++++++++------- dts/framework/testbed_model/node.py | 9 +++++++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/dts/conf.yaml b/dts/conf.yaml index 4b6965b3d7..c93eedbc94 100644 --- a/dts/conf.yaml +++ b/dts/conf.yaml @@ -40,8 +40,7 @@ nodes: hostname: sut1.change.me.localhost user: dtsuser os: linux - lcores: "" # use all the available logical cores - use_first_core: false # tells DPDK to use any physical core + lcores: "" # use all available logical cores (Skips first core) memory_channels: 4 # tells DPDK to use 4 memory channels hugepages_2mb: # optional; if removed, will use system hugepage configuration number_of: 256 diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 3fa8f4fa8f..5dfa0cf0d4 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -138,12 +138,12 @@ class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): #: A union type discriminating traffic generators by the `type` field. TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] -#: Comma-separated list of logical cores to use. An empty string means use all lcores. +#: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. LogicalCores = Annotated[ str, Field( - examples=["1,2,3,4,5,18-22", "10-15"], - pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$", + examples=["1,2,3,4,5,18-22", "10-15", "any"], + pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$|any", ), ] @@ -161,15 +161,20 @@ class NodeConfiguration(FrozenModel): password: str | None = None #: The operating system of the :class:`~framework.testbed_model.node.Node`. os: OS - #: A comma delimited list of logical cores to use when running DPDK. - lcores: LogicalCores = "1" - #: If :data:`True`, the first logical core won't be used. - use_first_core: bool = False + #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty + #: string or omitting this field means use any core except for the first one. The first core + #: will only be used if explicitly set. + lcores: LogicalCores = "" #: An optional hugepage configuration. hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb") #: The ports that can be used in testing. ports: list[PortConfig] = Field(min_length=1) + @property + def use_first_core(self) -> bool: + """Returns :data:`True` if `lcores` explicitly selects the first core.""" + return "0" in self.lcores + class SutNodeConfiguration(NodeConfiguration): """:class:`~framework.testbed_model.sut_node.SutNode` specific configuration.""" diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index 08328ee482..b08b1cf14d 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -91,6 +91,15 @@ def __init__(self, node_config: NodeConfiguration): self.lcores, LogicalCoreList(self.config.lcores) ).filter() + if LogicalCore(lcore=0, core=0, socket=0, node=0) in self.lcores: + self._logger.info( + """ + WARNING: First core being used; + using the first core is considered risky and should only + be done by advanced users. + """ + ) + self._other_sessions = [] self._init_ports() From patchwork Wed Jan 15 14:18:06 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150108 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id AE79E4608B; Wed, 15 Jan 2025 15:19:59 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 4E8F4402E5; Wed, 15 Jan 2025 15:19:40 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id 0F25C402DD for ; Wed, 15 Jan 2025 15:19:37 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id D818011FB; Wed, 15 Jan 2025 06:20:04 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id A00D23F63F; Wed, 15 Jan 2025 06:19:35 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 4/7] dts: rework DPDK attributes in SUT node config Date: Wed, 15 Jan 2025 14:18:06 +0000 Message-ID: <20250115141809.3898708-5-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org From: Nicholas Pratte Rework 'lcores' and 'memory_channels' into a new 'dpdk_config' subsection in an effort to make these attributes SUT specific; the traffic generator, more often than not, does not need this information. Ideally, if such information is needed, then it will be listed in the 'traffic_generator' component in TG Node configuration. Such logic is not introduced in this patch, but the framework can be rewritten to do so without any implications of extreme effort. To make this work, use_first_core has been removed from the framework entirely in favor of doing this within the LogicalCoreListFilter object. Since use_first_core was only ever activated when logical core 0 was explicitly defined, core 0 can be removed from the list of total logical cores assuming that it was not listed within filter_specifier. This patch also removes 'vdevs' from 'system_under_test_node' and moves it into 'test_runs'. Bugzilla ID: 1360 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx --- dts/conf.yaml | 20 +++++----- dts/framework/config/__init__.py | 39 ++++++++++---------- dts/framework/runner.py | 6 +-- dts/framework/testbed_model/cpu.py | 6 ++- dts/framework/testbed_model/linux_session.py | 5 +-- dts/framework/testbed_model/node.py | 27 +------------- dts/framework/testbed_model/os_session.py | 2 +- dts/framework/testbed_model/sut_node.py | 14 ++++++- 8 files changed, 54 insertions(+), 65 deletions(-) diff --git a/dts/conf.yaml b/dts/conf.yaml index c93eedbc94..bc78882d0d 100644 --- a/dts/conf.yaml +++ b/dts/conf.yaml @@ -26,12 +26,11 @@ test_runs: skip_smoke_tests: false # optional test_suites: # the following test suites will be run in their entirety - hello_world + vdevs: # optional; if removed, vdevs won't be used in the execution + - "crypto_openssl" # The machine running the DPDK test executable - system_under_test_node: - node_name: "SUT 1" - vdevs: # optional; if removed, vdevs won't be used in the test run - - "crypto_openssl" - # Traffic generator node to use for this test run + system_under_test_node: "SUT 1" + # Traffic generator node to use for this execution environment traffic_generator_node: "TG 1" nodes: # Define a system under test node, having two network ports physically @@ -40,11 +39,6 @@ nodes: hostname: sut1.change.me.localhost user: dtsuser os: linux - lcores: "" # use all available logical cores (Skips first core) - memory_channels: 4 # tells DPDK to use 4 memory channels - hugepages_2mb: # optional; if removed, will use system hugepage configuration - number_of: 256 - force_first_numa: false ports: # sets up the physical link between "SUT 1"@0000:00:08.0 and "TG 1"@0000:00:08.0 - pci: "0000:00:08.0" @@ -58,6 +52,12 @@ nodes: os_driver: i40e peer_node: "TG 1" peer_pci: "0000:00:08.1" + hugepages_2mb: # optional; if removed, will use system hugepage configuration + number_of: 256 + force_first_numa: false + dpdk_config: + lcores: "" # use all available logical cores (Skips first core) + memory_channels: 4 # tells DPDK to use 4 memory channels # Define a Scapy traffic generator node, having two network ports # physically connected to the corresponding ports in SUT 1 (the peer node). - name: "TG 1" diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 5dfa0cf0d4..2496f48e20 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -161,15 +161,23 @@ class NodeConfiguration(FrozenModel): password: str | None = None #: The operating system of the :class:`~framework.testbed_model.node.Node`. os: OS - #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty - #: string or omitting this field means use any core except for the first one. The first core - #: will only be used if explicitly set. - lcores: LogicalCores = "" #: An optional hugepage configuration. hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb") #: The ports that can be used in testing. ports: list[PortConfig] = Field(min_length=1) + +class DPDKConfiguration(FrozenModel): + """Configuration of the DPDK EAL parameters.""" + + #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty + #: string or omitting this field means use any core except for the first one. The first core + #: will only be used if explicitly set. + lcores: LogicalCores = "" + + #: The number of memory channels to use when running DPDK. + memory_channels: int = 1 + @property def use_first_core(self) -> bool: """Returns :data:`True` if `lcores` explicitly selects the first core.""" @@ -179,8 +187,8 @@ def use_first_core(self) -> bool: class SutNodeConfiguration(NodeConfiguration): """:class:`~framework.testbed_model.sut_node.SutNode` specific configuration.""" - #: The number of memory channels to use when running DPDK. - memory_channels: int = 1 + #: The runtime configuration for DPDK. + dpdk_config: DPDKConfiguration class TGNodeConfiguration(NodeConfiguration): @@ -405,15 +413,6 @@ def validate_names(self) -> Self: return self -class TestRunSUTNodeConfiguration(FrozenModel): - """The SUT node configuration of a test run.""" - - #: The SUT node to use in this test run. - node_name: str - #: The names of virtual devices to test. - vdevs: list[str] = Field(default_factory=list) - - class TestRunConfiguration(FrozenModel): """The configuration of a test run. @@ -431,10 +430,12 @@ class TestRunConfiguration(FrozenModel): skip_smoke_tests: bool = False #: The names of test suites and/or test cases to execute. test_suites: list[TestSuiteConfig] = Field(min_length=1) - #: The SUT node configuration to use in this test run. - system_under_test_node: TestRunSUTNodeConfiguration + #: The SUT node name to use in this test run. + system_under_test_node: str #: The TG node name to use in this test run. traffic_generator_node: str + #: The names of virtual devices to test. + vdevs: list[str] = Field(default_factory=list) #: The seed to use for pseudo-random generation. random_seed: int | None = None @@ -464,12 +465,12 @@ def test_runs_with_nodes(self) -> list[TestRunWithNodesConfiguration]: test_runs_with_nodes = [] for test_run_no, test_run in enumerate(self.test_runs): - sut_node_name = test_run.system_under_test_node.node_name + sut_node_name = test_run.system_under_test_node sut_node = next(filter(lambda n: n.name == sut_node_name, self.nodes), None) 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" + f"({test_run.system_under_test_node}) 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, " diff --git a/dts/framework/runner.py b/dts/framework/runner.py index 510be1a870..0cdbb07e06 100644 --- a/dts/framework/runner.py +++ b/dts/framework/runner.py @@ -277,7 +277,7 @@ def _connect_nodes_and_run_test_run( 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.node_name + failed_node = test_run_config.system_under_test_node if sut_node: failed_node = test_run_config.traffic_generator_node self._logger.exception(f"The Creation of node {failed_node} failed.") @@ -315,9 +315,7 @@ def _run_test_run( Raises: 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.node_name}'." - ) + self._logger.info(f"Running test run with SUT '{test_run_config.system_under_test_node}'.") test_run_result.ports = sut_node.ports test_run_result.sut_info = sut_node.node_info try: diff --git a/dts/framework/testbed_model/cpu.py b/dts/framework/testbed_model/cpu.py index d19fa5d597..b8bc601c22 100644 --- a/dts/framework/testbed_model/cpu.py +++ b/dts/framework/testbed_model/cpu.py @@ -185,7 +185,6 @@ def __init__( # sorting by core is needed in case hyperthreading is enabled self._lcores_to_filter = sorted(lcore_list, key=lambda x: x.core, reverse=not ascending) - self.filter() @abstractmethod def filter(self) -> list[LogicalCore]: @@ -228,6 +227,8 @@ def filter(self) -> list[LogicalCore]: Returns: The filtered cores. """ + if 0 in self._lcores_to_filter: + self._lcores_to_filter = self._lcores_to_filter[1:] sockets_to_filter = self._filter_sockets(self._lcores_to_filter) filtered_lcores = [] for socket_to_filter in sockets_to_filter: @@ -356,6 +357,9 @@ def filter(self) -> list[LogicalCore]: Raises: ValueError: If the specified lcore filter specifier is invalid. """ + if 0 not in self._filter_specifier.lcore_list: + self._lcores_to_filter = self._lcores_to_filter[1:] + if not len(self._filter_specifier.lcore_list): return self._lcores_to_filter diff --git a/dts/framework/testbed_model/linux_session.py b/dts/framework/testbed_model/linux_session.py index bda2d448f7..396bdbd890 100644 --- a/dts/framework/testbed_model/linux_session.py +++ b/dts/framework/testbed_model/linux_session.py @@ -67,15 +67,12 @@ class LinuxSession(PosixSession): def _get_privileged_command(command: str) -> str: return f"sudo -- sh -c '{command}'" - def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]: + def get_remote_cpus(self) -> list[LogicalCore]: """Overrides :meth:`~.os_session.OSSession.get_remote_cpus`.""" cpu_info = self.send_command("lscpu -p=CPU,CORE,SOCKET,NODE|grep -v \\#").stdout lcores = [] for cpu_line in cpu_info.splitlines(): lcore, core, socket, node = map(int, cpu_line.split(",")) - if core == 0 and socket == 0 and not use_first_core: - self._logger.info("Not using the first physical core.") - continue lcores.append(LogicalCore(lcore, core, socket, node)) return lcores diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index b08b1cf14d..6c2dfd6185 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -24,14 +24,7 @@ from framework.exception import ConfigurationError from framework.logger import DTSLogger, get_dts_logger -from .cpu import ( - Architecture, - LogicalCore, - LogicalCoreCount, - LogicalCoreList, - LogicalCoreListFilter, - lcore_filter, -) +from .cpu import Architecture, LogicalCore, LogicalCoreCount, LogicalCoreList, lcore_filter from .linux_session import LinuxSession from .os_session import OSSession from .port import Port @@ -82,24 +75,8 @@ def __init__(self, node_config: NodeConfiguration): self._logger = get_dts_logger(self.name) self.main_session = create_session(self.config, self.name, self._logger) self.arch = Architecture(self.main_session.get_arch_info()) - self._logger.info(f"Connected to node: {self.name}") - self._get_remote_cpus() - # filter the node lcores according to the test run configuration - self.lcores = LogicalCoreListFilter( - self.lcores, LogicalCoreList(self.config.lcores) - ).filter() - - if LogicalCore(lcore=0, core=0, socket=0, node=0) in self.lcores: - self._logger.info( - """ - WARNING: First core being used; - using the first core is considered risky and should only - be done by advanced users. - """ - ) - self._other_sessions = [] self._init_ports() @@ -188,7 +165,7 @@ def filter_lcores( def _get_remote_cpus(self) -> None: """Scan CPUs in the remote OS and store a list of LogicalCores.""" self._logger.info("Getting CPU information.") - self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core) + self.lcores = self.main_session.get_remote_cpus() def _setup_hugepages(self) -> None: """Setup hugepages on the node. diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py index fcda9b3de1..e436886692 100644 --- a/dts/framework/testbed_model/os_session.py +++ b/dts/framework/testbed_model/os_session.py @@ -445,7 +445,7 @@ def get_dpdk_version(self, version_path: str | PurePath) -> str: """ @abstractmethod - def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]: + def get_remote_cpus(self) -> list[LogicalCore]: r"""Get the list of :class:`~.cpu.LogicalCore`\s on the remote node. Args: diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py index 11d4b22089..d8f1f9d452 100644 --- a/dts/framework/testbed_model/sut_node.py +++ b/dts/framework/testbed_model/sut_node.py @@ -33,6 +33,7 @@ from framework.remote_session.remote_session import CommandResult from framework.utils import MesonArgs, TarCompressionFormat +from .cpu import LogicalCore, LogicalCoreList from .node import Node from .os_session import OSSession, OSSessionInfo from .virtual_device import VirtualDevice @@ -92,6 +93,17 @@ def __init__(self, node_config: SutNodeConfiguration): node_config: The SUT node's test run configuration. """ super().__init__(node_config) + self.lcores = self.filter_lcores(LogicalCoreList(self.config.dpdk_config.lcores)) + if LogicalCore(lcore=0, core=0, socket=0, node=0) in self.lcores: + self._logger.info( + """ + WARNING: First core being used; + using the first core is considered risky and should only + be done by advanced users. + """ + ) + else: + self._logger.info("Not using first core") self.virtual_devices = [] self.dpdk_prefix_list = [] self._env_vars = {} @@ -198,7 +210,7 @@ def set_up_test_run( dpdk_build_config: The build configuration of DPDK. """ super().set_up_test_run(test_run_config, dpdk_build_config) - for vdev in test_run_config.system_under_test_node.vdevs: + for vdev in test_run_config.vdevs: self.virtual_devices.append(VirtualDevice(vdev)) self._set_up_dpdk(dpdk_build_config) From patchwork Wed Jan 15 14:18:07 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150109 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id D40854608B; Wed, 15 Jan 2025 15:20:05 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 891CF40611; Wed, 15 Jan 2025 15:19:41 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id 2983B402EF for ; Wed, 15 Jan 2025 15:19:38 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id F077912FC; Wed, 15 Jan 2025 06:20:05 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id C90083F63F; Wed, 15 Jan 2025 06:19:36 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 5/7] dts: handle CLI overrides in the configuration Date: Wed, 15 Jan 2025 14:18:07 +0000 Message-ID: <20250115141809.3898708-6-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org The current handling of the configuration loading is inconsistent. After the whole configuration is loaded, if there are any CLI or environment overrides set, the code forcefully modifies the frozen configuration to use them. This changes the handling by passing the environment/CLI settings as part of the configuration context and handle the overrides directly at the field level before these are validated. As a positive side effect, the validator won't complain if a field is missing from the file but it has been specified as an environment/CLI override. Bugzilla ID: 1360 Bugzilla ID: 1598 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx --- dts/framework/config/__init__.py | 40 ++++++++++++++++++++++++++++---- dts/framework/runner.py | 18 +++----------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 2496f48e20..5510e3547b 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -36,7 +36,7 @@ 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 +from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple, TypedDict, cast import yaml from pydantic import ( @@ -44,18 +44,36 @@ ConfigDict, Field, ValidationError, + ValidationInfo, field_validator, model_validator, ) from typing_extensions import Self from framework.exception import ConfigurationError +from framework.settings import Settings from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum if TYPE_CHECKING: from framework.test_suite import TestSuiteSpec +class ValidationContext(TypedDict): + """A context dictionary to use for validation.""" + + #: The command line settings. + settings: Settings + + +def load_from_settings(data: Any, info: ValidationInfo): + """Before field validator that injects values from :attr:`ValidationContext.settings`.""" + context = cast(ValidationContext, info.context) + assert info.field_name is not None, "This validator can only be used as a field validator." + if settings_data := getattr(context["settings"], info.field_name): + return settings_data + return data + + class FrozenModel(BaseModel): """A pre-configured :class:`~pydantic.BaseModel`.""" @@ -317,6 +335,10 @@ class BaseDPDKBuildConfiguration(FrozenModel): #: The location of the DPDK tree. dpdk_location: DPDKLocation + dpdk_location_from_settings = field_validator("dpdk_location", mode="before")( + load_from_settings + ) + class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): """DPDK precompiled build configuration.""" @@ -325,6 +347,10 @@ class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): #: subdirectory of `~dpdk_location.dpdk_tree` or `~dpdk_location.tarball` root directory. precompiled_build_dir: str = Field(min_length=1) + build_dir_from_settings = field_validator("precompiled_build_dir", mode="before")( + load_from_settings + ) + class DPDKBuildOptionsConfiguration(FrozenModel): """DPDK build options configuration. @@ -439,6 +465,10 @@ class TestRunConfiguration(FrozenModel): #: The seed to use for pseudo-random generation. random_seed: int | None = None + fields_from_settings = field_validator("test_suites", "random_seed", mode="before")( + load_from_settings + ) + class TestRunWithNodesConfiguration(NamedTuple): """Tuple containing the configuration of the test run and its associated nodes.""" @@ -541,7 +571,7 @@ def validate_test_runs_with_nodes(self) -> Self: return self -def load_config(config_file_path: Path) -> Configuration: +def load_config(settings: Settings) -> Configuration: """Load DTS test run configuration from a file. Load the YAML test run configuration file, validate it, and create a test run configuration @@ -552,6 +582,7 @@ def load_config(config_file_path: Path) -> Configuration: Args: config_file_path: The path to the YAML test run configuration file. + settings: The settings provided by the user on the command line. Returns: The parsed test run configuration. @@ -559,10 +590,11 @@ def load_config(config_file_path: Path) -> Configuration: Raises: ConfigurationError: If the supplied configuration file is invalid. """ - with open(config_file_path, "r") as f: + with open(settings.config_file_path, "r") as f: config_data = yaml.safe_load(f) try: - return Configuration.model_validate(config_data) + context = ValidationContext(settings=settings) + return Configuration.model_validate(config_data, context=context) except ValidationError as e: raise ConfigurationError("failed to load the supplied configuration") from e diff --git a/dts/framework/runner.py b/dts/framework/runner.py index 0cdbb07e06..e46a8c1a4f 100644 --- a/dts/framework/runner.py +++ b/dts/framework/runner.py @@ -31,7 +31,6 @@ from .config import ( Configuration, - DPDKPrecompiledBuildConfiguration, SutNodeConfiguration, TestRunConfiguration, TestSuiteConfig, @@ -82,7 +81,7 @@ class DTSRunner: def __init__(self): """Initialize the instance with configuration, logger, result and string constants.""" - self._configuration = load_config(SETTINGS.config_file_path) + self._configuration = load_config(SETTINGS) self._logger = get_dts_logger() if not os.path.exists(SETTINGS.output_dir): os.makedirs(SETTINGS.output_dir) @@ -142,9 +141,7 @@ def run(self) -> None: 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 - test_run_test_suites = list( - SETTINGS.test_suites if SETTINGS.test_suites else test_run_config.test_suites - ) + test_run_test_suites = test_run_config.test_suites if not test_run_config.skip_smoke_tests: test_run_test_suites[:0] = [TestSuiteConfig(test_suite="smoke_tests")] try: @@ -320,15 +317,6 @@ def _run_test_run( test_run_result.sut_info = sut_node.node_info try: 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.dpdk_build_info = sut_node.get_dpdk_build_info() tg_node.set_up_test_run(test_run_config, dpdk_build_config) @@ -622,6 +610,6 @@ def _exit_dts(self) -> None: def _init_random_seed(self, conf: TestRunConfiguration) -> None: """Initialize the random seed to use for the test run.""" - seed = SETTINGS.random_seed or conf.random_seed or random.randrange(0xFFFF_FFFF) + seed = conf.random_seed or random.randrange(0xFFFF_FFFF) self._logger.info(f"Initializing test run with random seed {seed}.") random.seed(seed) From patchwork Wed Jan 15 14:18:08 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150110 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id C04DD4608B; Wed, 15 Jan 2025 15:20:13 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 4FEF240648; Wed, 15 Jan 2025 15:19:43 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id 7B0724042C for ; Wed, 15 Jan 2025 15:19:39 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 3D22211FB; Wed, 15 Jan 2025 06:20:07 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id E36BB3F63F; Wed, 15 Jan 2025 06:19:37 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 6/7] dts: split configuration file Date: Wed, 15 Jan 2025 14:18:08 +0000 Message-ID: <20250115141809.3898708-7-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org To avoid the creation of a big monolithic configuration file, nodes and test runs are now split into distinct files. This also allows flexibility to run different test runs on the same nodes. Since there are now 2 distinct configuration files, there are also 2 command line arguments to specify them. Bugzilla ID: 1344 Signed-off-by: Nicholas Pratte Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx --- doc/guides/tools/dts.rst | 78 ++- dts/.gitignore | 4 + dts/conf.yaml | 84 --- dts/framework/config/__init__.py | 501 ++---------------- dts/framework/config/common.py | 34 ++ dts/framework/config/node.py | 144 +++++ dts/framework/config/test_run.py | 290 ++++++++++ dts/framework/runner.py | 11 +- dts/framework/settings.py | 37 +- dts/framework/test_result.py | 2 +- dts/framework/testbed_model/node.py | 6 +- dts/framework/testbed_model/os_session.py | 2 +- dts/framework/testbed_model/port.py | 2 +- dts/framework/testbed_model/sut_node.py | 6 +- dts/framework/testbed_model/tg_node.py | 2 +- dts/framework/testbed_model/topology.py | 2 +- .../traffic_generator/__init__.py | 2 +- .../testbed_model/traffic_generator/scapy.py | 2 +- .../traffic_generator/traffic_generator.py | 2 +- dts/nodes.example.yaml | 53 ++ dts/test_runs.example.yaml | 33 ++ dts/tests/TestSuite_smoke_tests.py | 2 +- 22 files changed, 704 insertions(+), 595 deletions(-) create mode 100644 dts/.gitignore delete mode 100644 dts/conf.yaml create mode 100644 dts/framework/config/common.py create mode 100644 dts/framework/config/node.py create mode 100644 dts/framework/config/test_run.py create mode 100644 dts/nodes.example.yaml create mode 100644 dts/test_runs.example.yaml diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst index abc389b42a..6fc4eb8dac 100644 --- a/doc/guides/tools/dts.rst +++ b/doc/guides/tools/dts.rst @@ -210,8 +210,10 @@ DTS configuration is split into nodes and test runs, and must respect the model definitions as documented in the DTS API docs under the ``config`` page. The root of the configuration is represented by the ``Configuration`` model. -By default, DTS will try to use the ``dts/conf.yaml`` :ref:`config file `, -which is a template that illustrates what can be configured in DTS. +By default, DTS will try to use the ``dts/test_runs.example.yaml`` +:ref:`config file `, and ``dts/nodes.example.yaml`` +:ref:`config file ` which are templates that +illustrate what can be configured in DTS. The user must have :ref:`administrator privileges ` which don't require password authentication. @@ -225,16 +227,19 @@ DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet .. code-block:: console (dts-py3.10) $ ./main.py --help - usage: main.py [-h] [--config-file FILE_PATH] [--output-dir DIR_PATH] [-t SECONDS] [-v] [--dpdk-tree DIR_PATH | --tarball FILE_PATH] [--remote-source] - [--precompiled-build-dir DIR_NAME] [--compile-timeout SECONDS] [--test-suite TEST_SUITE [TEST_CASES ...]] [--re-run N_TIMES] - [--random-seed NUMBER] + usage: main.py [-h] [--test-runs-config-file FILE_PATH] [--nodes-config-file FILE_PATH] [--output-dir DIR_PATH] [-t SECONDS] [-v] + [--dpdk-tree DIR_PATH | --tarball FILE_PATH] [--remote-source] [--precompiled-build-dir DIR_NAME] + [--compile-timeout SECONDS] [--test-suite TEST_SUITE [TEST_CASES ...]] [--re-run N_TIMES] [--random-seed NUMBER] - Run DPDK test suites. All options may be specified with the environment variables provided in brackets. Command line arguments have higher priority. + Run DPDK test suites. All options may be specified with the environment variables provided in brackets. Command line arguments have higher + priority. options: -h, --help show this help message and exit - --config-file FILE_PATH - [DTS_CFG_FILE] The configuration file that describes the test cases, SUTs and DPDK build configs. (default: conf.yaml) + --test-runs-config-file FILE_PATH + [DTS_TEST_RUNS_CFG_FILE] The configuration file that describes the test cases and DPDK build options. (default: test-runs.conf.yaml) + --nodes-config-file FILE_PATH + [DTS_NODES_CFG_FILE] The configuration file that describes the SUT and TG nodes. (default: nodes.conf.yaml) --output-dir DIR_PATH, --output DIR_PATH [DTS_OUTPUT_DIR] Output directory where DTS logs and results are saved. (default: output) -t SECONDS, --timeout SECONDS @@ -243,31 +248,31 @@ DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet --compile-timeout SECONDS [DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK. (default: 1200) --test-suite TEST_SUITE [TEST_CASES ...] - [DTS_TEST_SUITES] A list containing a test suite with test cases. The first parameter is the test suite name, and the rest are - test case names, which are optional. May be specified multiple times. To specify multiple test suites in the environment - variable, join the lists with a comma. Examples: --test-suite suite case case --test-suite suite case ... | - DTS_TEST_SUITES='suite case case, suite case, ...' | --test-suite suite --test-suite suite case ... | DTS_TEST_SUITES='suite, - suite case, ...' (default: []) + [DTS_TEST_SUITES] A list containing a test suite with test cases. The first parameter is the test suite name, and + the rest are test case names, which are optional. May be specified multiple times. To specify multiple test suites + in the environment variable, join the lists with a comma. Examples: --test-suite suite case case --test-suite + suite case ... | DTS_TEST_SUITES='suite case case, suite case, ...' | --test-suite suite --test-suite suite case + ... | DTS_TEST_SUITES='suite, suite case, ...' (default: []) --re-run N_TIMES, --re_run N_TIMES [DTS_RERUN] Re-run each test case the specified number of times if a test failure occurs. (default: 0) - --random-seed NUMBER [DTS_RANDOM_SEED] The seed to use with the pseudo-random generator. If not specified, the configuration value is used instead. - If that's also not specified, a random seed is generated. (default: None) + --random-seed NUMBER [DTS_RANDOM_SEED] The seed to use with the pseudo-random generator. If not specified, the configuration value is + used instead. If that's also not specified, a random seed is generated. (default: None) DPDK Build Options: - Arguments in this group (and subgroup) will be applied to a DPDKLocation when the DPDK tree, tarball or revision will be provided, other arguments - like remote source and build dir are optional. A DPDKLocation from settings are used instead of from config if construct successful. + Arguments in this group (and subgroup) will be applied to a DPDKLocation when the DPDK tree, tarball or revision will be provided, + other arguments like remote source and build dir are optional. A DPDKLocation from settings are used instead of from config if + construct successful. - --dpdk-tree DIR_PATH [DTS_DPDK_TREE] The path to the DPDK source tree directory to test. Cannot be used in conjunction with --tarball. (default: - None) + --dpdk-tree DIR_PATH [DTS_DPDK_TREE] The path to the DPDK source tree directory to test. Cannot be used in conjunction with --tarball. + (default: None) --tarball FILE_PATH, --snapshot FILE_PATH - [DTS_DPDK_TARBALL] The path to the DPDK source tarball to test. DPDK must be contained in a folder with the same name as the - tarball file. Cannot be used in conjunction with --dpdk-tree. (default: None) - --remote-source [DTS_REMOTE_SOURCE] Set this option if either the DPDK source tree or tarball to be used are located on the SUT node. Can only - be used with --dpdk-tree or --tarball. (default: False) + [DTS_DPDK_TARBALL] The path to the DPDK source tarball to test. DPDK must be contained in a folder with the same + name as the tarball file. Cannot be used in conjunction with --dpdk-tree. (default: None) + --remote-source [DTS_REMOTE_SOURCE] Set this option if either the DPDK source tree or tarball to be used are located on the SUT + node. Can only be used with --dpdk-tree or --tarball. (default: False) --precompiled-build-dir DIR_NAME - [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. - (default: None) + [DTS_PRECOMPILED_BUILD_DIR] Define the subdirectory under the DPDK tree root directory or tarball where the pre- + compiled binaries are located. (default: None) The brackets contain the names of environment variables that set the same thing. @@ -467,7 +472,7 @@ The output is generated in ``build/doc/api/dts/html``. Configuration Example --------------------- -The following example (which can be found in ``dts/conf.yaml``) sets up two nodes: +The following example configuration files sets up two nodes: * ``SUT1`` which is already setup with the DPDK build requirements and any other required for execution; @@ -479,6 +484,21 @@ And they both have two network ports which are physically connected to each othe This example assumes that you have setup SSH keys in both the system under test and traffic generator nodes. -.. literalinclude:: ../../../dts/conf.yaml +.. _test_runs_configuration_example: + +``dts/test_runs.example.yaml`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. literalinclude:: ../../../dts/test_runs.example.yaml + :language: yaml + :start-at: # Define + +.. _nodes_configuration_example: + + +``dts/nodes.example.yaml`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. literalinclude:: ../../../dts/nodes.example.yaml :language: yaml - :start-at: test_runs: + :start-at: # Define diff --git a/dts/.gitignore b/dts/.gitignore new file mode 100644 index 0000000000..d53a2f3b7e --- /dev/null +++ b/dts/.gitignore @@ -0,0 +1,4 @@ +# default configuration files for DTS +nodes.yaml +test_runs.yaml + diff --git a/dts/conf.yaml b/dts/conf.yaml deleted file mode 100644 index bc78882d0d..0000000000 --- a/dts/conf.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright 2022-2023 The DPDK contributors -# Copyright 2023 Arm Limited - -test_runs: - # define one test run environment - - dpdk_build: - 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: - # the combination of the following two makes CC="ccache gcc" - compiler: gcc - compiler_wrapper: ccache # Optional. - # If `precompiled_build_dir` is defined, DPDK has been pre-built and the build directory is - # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` - # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be - # defined, but not both. - perf: false # disable performance testing - func: true # enable functional testing - skip_smoke_tests: false # optional - test_suites: # the following test suites will be run in their entirety - - hello_world - vdevs: # optional; if removed, vdevs won't be used in the execution - - "crypto_openssl" - # The machine running the DPDK test executable - system_under_test_node: "SUT 1" - # Traffic generator node to use for this execution environment - traffic_generator_node: "TG 1" -nodes: - # Define a system under test node, having two network ports physically - # connected to the corresponding ports in TG 1 (the peer node) - - name: "SUT 1" - hostname: sut1.change.me.localhost - user: dtsuser - os: linux - ports: - # sets up the physical link between "SUT 1"@0000:00:08.0 and "TG 1"@0000:00:08.0 - - pci: "0000:00:08.0" - os_driver_for_dpdk: vfio-pci # OS driver that DPDK will use - os_driver: i40e # OS driver to bind when the tests are not running - peer_node: "TG 1" - peer_pci: "0000:00:08.0" - # sets up the physical link between "SUT 1"@0000:00:08.1 and "TG 1"@0000:00:08.1 - - pci: "0000:00:08.1" - os_driver_for_dpdk: vfio-pci - os_driver: i40e - peer_node: "TG 1" - peer_pci: "0000:00:08.1" - hugepages_2mb: # optional; if removed, will use system hugepage configuration - number_of: 256 - force_first_numa: false - dpdk_config: - lcores: "" # use all available logical cores (Skips first core) - memory_channels: 4 # tells DPDK to use 4 memory channels - # Define a Scapy traffic generator node, having two network ports - # physically connected to the corresponding ports in SUT 1 (the peer node). - - name: "TG 1" - hostname: tg1.change.me.localhost - user: dtsuser - os: linux - ports: - # sets up the physical link between "TG 1"@0000:00:08.0 and "SUT 1"@0000:00:08.0 - - pci: "0000:00:08.0" - os_driver_for_dpdk: rdma - os_driver: rdma - peer_node: "SUT 1" - peer_pci: "0000:00:08.0" - # sets up the physical link between "SUT 1"@0000:00:08.0 and "TG 1"@0000:00:08.0 - - pci: "0000:00:08.1" - os_driver_for_dpdk: rdma - os_driver: rdma - peer_node: "SUT 1" - peer_pci: "0000:00:08.1" - hugepages_2mb: # optional; if removed, will use system hugepage configuration - number_of: 256 - force_first_numa: false - traffic_generator: - type: SCAPY diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index 5510e3547b..adbd4e952d 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -8,20 +8,15 @@ 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 against the :class:`Configuration` Pydantic -model. +the YAML configuration files and validates them against the :class:`Configuration` Pydantic +model, which fields are directly mapped. -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 map directly to the -:class:`Configuration` model, its fields and sub-models. +The configuration files are split in: -The test run configuration has two main sections: - - * The :class:`TestRunConfiguration` which defines what tests are going to be run - and how DPDK will be built. It also references the testbed where these tests and DPDK - are going to be run, - * The nodes of the testbed are defined in the other section, - a :class:`list` of :class:`NodeConfiguration` objects. + * A list of test run which are represented by :class:`~.test_run.TestRunConfiguration` + defining what tests are going to be run and how DPDK will be built. It also references + the testbed where these tests and DPDK are going to be run, + * A list of the nodes of the testbed which ar represented by :class:`~.node.NodeConfiguration`. The real-time information about testbed is supposed to be gathered at runtime. @@ -32,442 +27,24 @@ and makes it thread safe should we ever want to move in that direction. """ -import tarfile -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, TypedDict, cast +from pathlib import Path +from typing import Annotated, Any, Literal, NamedTuple, TypeVar, cast import yaml -from pydantic import ( - BaseModel, - ConfigDict, - Field, - ValidationError, - ValidationInfo, - field_validator, - model_validator, -) +from pydantic import Field, TypeAdapter, ValidationError, field_validator, model_validator from typing_extensions import Self from framework.exception import ConfigurationError -from framework.settings import Settings -from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum - -if TYPE_CHECKING: - from framework.test_suite import TestSuiteSpec - - -class ValidationContext(TypedDict): - """A context dictionary to use for validation.""" - - #: The command line settings. - settings: Settings - - -def load_from_settings(data: Any, info: ValidationInfo): - """Before field validator that injects values from :attr:`ValidationContext.settings`.""" - context = cast(ValidationContext, info.context) - assert info.field_name is not None, "This validator can only be used as a field validator." - if settings_data := getattr(context["settings"], info.field_name): - return settings_data - return data - - -class FrozenModel(BaseModel): - """A pre-configured :class:`~pydantic.BaseModel`.""" - - #: Fields are set as read-only and any extra fields are forbidden. - model_config = ConfigDict(frozen=True, extra="forbid") - - -@unique -class OS(StrEnum): - r"""The supported operating systems of :class:`~framework.testbed_model.node.Node`\s.""" - - #: - linux = auto() - #: - freebsd = auto() - #: - windows = auto() - - -@unique -class Compiler(StrEnum): - r"""The supported compilers of :class:`~framework.testbed_model.node.Node`\s.""" - - #: - gcc = auto() - #: - clang = auto() - #: - icc = auto() - #: - msvc = auto() - - -@unique -class TrafficGeneratorType(str, Enum): - """The supported traffic generators.""" - - #: - SCAPY = "SCAPY" - - -class HugepageConfiguration(FrozenModel): - r"""The hugepage configuration of :class:`~framework.testbed_model.node.Node`\s.""" - - #: The number of hugepages to allocate. - number_of: int - #: If :data:`True`, the hugepages will be configured on the first NUMA node. - force_first_numa: bool - - -class PortConfig(FrozenModel): - r"""The port configuration of :class:`~framework.testbed_model.node.Node`\s.""" - - #: The PCI address of the port. - pci: str = Field(pattern=REGEX_FOR_PCI_ADDRESS) - #: The driver that the kernel should bind this device to for DPDK to use it. - os_driver_for_dpdk: str = Field(examples=["vfio-pci", "mlx5_core"]) - #: The operating system driver name when the operating system controls the port. - os_driver: str = Field(examples=["i40e", "ice", "mlx5_core"]) - #: The name of the peer node this port is connected to. - peer_node: str - #: The PCI address of the peer port connected to this port. - peer_pci: str = Field(pattern=REGEX_FOR_PCI_ADDRESS) - - -class TrafficGeneratorConfig(FrozenModel): - """A protocol required to define traffic generator types.""" - - #: The traffic generator type the child class is required to define to be distinguished among - #: others. - type: TrafficGeneratorType - - -class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): - """Scapy traffic generator specific configuration.""" - - type: Literal[TrafficGeneratorType.SCAPY] - - -#: A union type discriminating traffic generators by the `type` field. -TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] - -#: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. -LogicalCores = Annotated[ - str, - Field( - examples=["1,2,3,4,5,18-22", "10-15", "any"], - pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$|any", - ), -] - - -class NodeConfiguration(FrozenModel): - r"""The configuration of :class:`~framework.testbed_model.node.Node`\s.""" - - #: The name of the :class:`~framework.testbed_model.node.Node`. - name: str - #: The hostname of the :class:`~framework.testbed_model.node.Node`. Can also be an IP address. - hostname: str - #: The name of the user used to connect to the :class:`~framework.testbed_model.node.Node`. - user: str - #: The password of the user. The use of passwords is heavily discouraged, please use SSH keys. - password: str | None = None - #: The operating system of the :class:`~framework.testbed_model.node.Node`. - os: OS - #: An optional hugepage configuration. - hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb") - #: The ports that can be used in testing. - ports: list[PortConfig] = Field(min_length=1) - - -class DPDKConfiguration(FrozenModel): - """Configuration of the DPDK EAL parameters.""" - - #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty - #: string or omitting this field means use any core except for the first one. The first core - #: will only be used if explicitly set. - lcores: LogicalCores = "" - - #: The number of memory channels to use when running DPDK. - memory_channels: int = 1 - - @property - def use_first_core(self) -> bool: - """Returns :data:`True` if `lcores` explicitly selects the first core.""" - return "0" in self.lcores - - -class SutNodeConfiguration(NodeConfiguration): - """:class:`~framework.testbed_model.sut_node.SutNode` specific configuration.""" - - #: The runtime configuration for DPDK. - dpdk_config: DPDKConfiguration - - -class TGNodeConfiguration(NodeConfiguration): - """:class:`~framework.testbed_model.tg_node.TGNode` specific configuration.""" - - #: The configuration of the traffic generator present on the TG node. - traffic_generator: TrafficGeneratorConfigTypes - - -#: Union type for all the node configuration types. -NodeConfigurationTypes = TGNodeConfiguration | SutNodeConfiguration - - -def resolve_path(path: Path) -> Path: - """Resolve a path into a real path.""" - return path.resolve() - - -class BaseDPDKLocation(FrozenModel): - """DPDK location base class. - - The path to the DPDK sources and type of location. - """ - - #: Specifies whether to find DPDK on the SUT node or on the local host. Which are respectively - #: represented by :class:`RemoteDPDKLocation` and :class:`LocalDPDKTreeLocation`. - remote: bool = False - - -class LocalDPDKLocation(BaseDPDKLocation): - """Local DPDK location base class. - - This class is meant to represent any location that is present only locally. - """ - - remote: Literal[False] = False - - -class LocalDPDKTreeLocation(LocalDPDKLocation): - """Local DPDK tree location. - - This class makes a distinction from :class:`RemoteDPDKTreeLocation` by enforcing on the fly - validation. - """ - - #: The path to the DPDK source tree directory on the local host passed as string. - dpdk_tree: Path - - #: Resolve the local DPDK tree path. - resolve_dpdk_tree_path = field_validator("dpdk_tree")(resolve_path) - - @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 - - -class LocalDPDKTarballLocation(LocalDPDKLocation): - """Local DPDK tarball location. - - This class makes a distinction from :class:`RemoteDPDKTarballLocation` by enforcing on the fly - validation. - """ - - #: The path to the DPDK tarball on the local host passed as string. - tarball: Path - - #: Resolve the local tarball path. - resolve_tarball_path = field_validator("tarball")(resolve_path) - - @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): - """Remote DPDK location base class. - - This class is meant to represent any location that is present only remotely. - """ - - remote: Literal[True] = True - - -class RemoteDPDKTreeLocation(RemoteDPDKLocation): - """Remote DPDK tree location. - - This class is distinct from :class:`LocalDPDKTreeLocation` which enforces on the fly validation. - """ - #: The path to the DPDK source tree directory on the remote node passed as string. - dpdk_tree: PurePath - - -class RemoteDPDKTarballLocation(RemoteDPDKLocation): - """Remote DPDK tarball location. - - This class is distinct from :class:`LocalDPDKTarballLocation` which enforces on the fly - validation. - """ - - #: The path to the DPDK tarball on the remote node passed as string. - tarball: PurePath - - -#: Union type for different DPDK locations. -DPDKLocation = ( - LocalDPDKTreeLocation - | LocalDPDKTarballLocation - | RemoteDPDKTreeLocation - | RemoteDPDKTarballLocation +from .common import FrozenModel, ValidationContext +from .node import ( + NodeConfiguration, + NodeConfigurationTypes, + SutNodeConfiguration, + TGNodeConfiguration, ) - - -class BaseDPDKBuildConfiguration(FrozenModel): - """The base configuration for different types of build. - - The configuration contain the location of the DPDK and configuration used for building it. - """ - - #: The location of the DPDK tree. - dpdk_location: DPDKLocation - - dpdk_location_from_settings = field_validator("dpdk_location", mode="before")( - load_from_settings - ) - - -class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): - """DPDK precompiled build configuration.""" - - #: If it's defined, DPDK has been pre-compiled and the build directory is located in a - #: subdirectory of `~dpdk_location.dpdk_tree` or `~dpdk_location.tarball` root directory. - precompiled_build_dir: str = Field(min_length=1) - - build_dir_from_settings = field_validator("precompiled_build_dir", mode="before")( - load_from_settings - ) - - -class DPDKBuildOptionsConfiguration(FrozenModel): - """DPDK build options configuration. - - The build options used for building DPDK. - """ - - #: The compiler executable to use. - compiler: Compiler - #: This string will be put in front of the compiler when executing the build. Useful for adding - #: wrapper commands, such as ``ccache``. - compiler_wrapper: str = "" - - -class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration): - """DPDK uncompiled build configuration.""" - - #: The build options to compiled DPDK with. - build_options: DPDKBuildOptionsConfiguration - - -#: Union type for different build configurations. -DPDKBuildConfiguration = DPDKPrecompiledBuildConfiguration | DPDKUncompiledBuildConfiguration - - -class TestSuiteConfig(FrozenModel): - """Test suite configuration. - - 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 - """ - - #: The name of the test suite module without the starting ``TestSuite_``. - test_suite_name: str = Field(alias="test_suite") - #: The names of test cases from this test suite to execute. If empty, all test cases will be - #: executed. - test_cases_names: list[str] = Field(default_factory=list, 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 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}." - ) - - return self - - -class TestRunConfiguration(FrozenModel): - """The configuration of a test run. - - The configuration contains testbed information, what tests to execute - and with what DPDK build. - """ - - #: The DPDK configuration used to test. - dpdk_config: DPDKBuildConfiguration = Field(alias="dpdk_build") - #: Whether to run performance tests. - perf: bool - #: Whether to run functional tests. - func: bool - #: Whether to skip smoke tests. - skip_smoke_tests: bool = False - #: The names of test suites and/or test cases to execute. - test_suites: list[TestSuiteConfig] = Field(min_length=1) - #: The SUT node name to use in this test run. - system_under_test_node: str - #: The TG node name to use in this test run. - traffic_generator_node: str - #: The names of virtual devices to test. - vdevs: list[str] = Field(default_factory=list) - #: The seed to use for pseudo-random generation. - random_seed: int | None = None - - fields_from_settings = field_validator("test_suites", "random_seed", mode="before")( - load_from_settings - ) +from .test_run import TestRunConfiguration class TestRunWithNodesConfiguration(NamedTuple): @@ -481,13 +58,18 @@ class TestRunWithNodesConfiguration(NamedTuple): tg_node_config: TGNodeConfiguration +TestRunsConfig = Annotated[list[TestRunConfiguration], Field(min_length=1)] + +NodesConfig = Annotated[list[NodeConfigurationTypes], Field(min_length=1)] + + class Configuration(FrozenModel): """DTS testbed and test configuration.""" #: Test run configurations. - test_runs: list[TestRunConfiguration] = Field(min_length=1) + test_runs: TestRunsConfig #: Node configurations. - nodes: list[NodeConfigurationTypes] = Field(min_length=1) + nodes: NodesConfig @cached_property def test_runs_with_nodes(self) -> list[TestRunWithNodesConfiguration]: @@ -571,30 +153,37 @@ def validate_test_runs_with_nodes(self) -> Self: return self -def load_config(settings: Settings) -> Configuration: - """Load DTS test run configuration from a file. +T = TypeVar("T") + + +def _load_and_parse_model(file_path: Path, model_type: T, ctx: ValidationContext) -> T: + with open(file_path) as f: + try: + data = yaml.safe_load(f) + return TypeAdapter(model_type).validate_python(data, context=cast(dict[str, Any], ctx)) + except ValidationError as e: + msg = f"failed to load the configuration file {file_path}" + raise ConfigurationError(msg) from e + - Load the YAML test run configuration file, validate it, and create a test run configuration - object. +def load_config(ctx: ValidationContext) -> Configuration: + """Load the DTS configuration from files. - The YAML test run configuration file is specified in the :option:`--config-file` command line - argument or the :envvar:`DTS_CFG_FILE` environment variable. + Load the YAML configuration files, validate them, and create a configuration object. Args: - config_file_path: The path to the YAML test run configuration file. - settings: The settings provided by the user on the command line. + ctx: The context required for validation. Returns: The parsed test run configuration. Raises: - ConfigurationError: If the supplied configuration file is invalid. + ConfigurationError: If the supplied configuration files are invalid. """ - with open(settings.config_file_path, "r") as f: - config_data = yaml.safe_load(f) + test_runs = _load_and_parse_model(ctx["settings"].test_runs_config_path, TestRunsConfig, ctx) + nodes = _load_and_parse_model(ctx["settings"].nodes_config_path, NodesConfig, ctx) try: - context = ValidationContext(settings=settings) - return Configuration.model_validate(config_data, context=context) + return Configuration.model_validate({"test_runs": test_runs, "nodes": nodes}, context=ctx) except ValidationError as e: - raise ConfigurationError("failed to load the supplied configuration") from e + raise ConfigurationError("the configurations supplied are invalid") from e diff --git a/dts/framework/config/common.py b/dts/framework/config/common.py new file mode 100644 index 0000000000..fea8af4a11 --- /dev/null +++ b/dts/framework/config/common.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Arm Limited + +"""Common definitions and objects for the configuration.""" + +from typing import TYPE_CHECKING, Any, TypedDict, cast + +from pydantic import BaseModel, ConfigDict, ValidationInfo + +if TYPE_CHECKING: + from framework.settings import Settings + + +class ValidationContext(TypedDict): + """A context dictionary to use for validation.""" + + #: The command line settings. + settings: "Settings" + + +def load_from_settings(data: Any, info: ValidationInfo): + """Before field validator that injects values from :attr:`ValidationContext.settings`.""" + context = cast(ValidationContext, info.context) + assert info.field_name is not None, "This validator can only be used as a field validator." + if settings_data := getattr(context["settings"], info.field_name): + return settings_data + return data + + +class FrozenModel(BaseModel): + """A pre-configured :class:`~pydantic.BaseModel`.""" + + #: Fields are set as read-only and any extra fields are forbidden. + model_config = ConfigDict(frozen=True, extra="forbid") diff --git a/dts/framework/config/node.py b/dts/framework/config/node.py new file mode 100644 index 0000000000..a7ace514d9 --- /dev/null +++ b/dts/framework/config/node.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: BSD-3-Clause +# 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 + +"""Configuration models representing a node. + +The root model of a node configuration is :class:`NodeConfiguration`. +""" + +from enum import Enum, auto, unique +from typing import Annotated, Literal + +from pydantic import Field + +from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum + +from .common import FrozenModel + + +@unique +class OS(StrEnum): + r"""The supported operating systems of :class:`~framework.testbed_model.node.Node`\s.""" + + #: + linux = auto() + #: + freebsd = auto() + #: + windows = auto() + + +@unique +class TrafficGeneratorType(str, Enum): + """The supported traffic generators.""" + + #: + SCAPY = "SCAPY" + + +class HugepageConfiguration(FrozenModel): + r"""The hugepage configuration of :class:`~framework.testbed_model.node.Node`\s.""" + + #: The number of hugepages to allocate. + number_of: int + #: If :data:`True`, the hugepages will be configured on the first NUMA node. + force_first_numa: bool + + +class PortConfig(FrozenModel): + r"""The port configuration of :class:`~framework.testbed_model.node.Node`\s.""" + + #: The PCI address of the port. + pci: str = Field(pattern=REGEX_FOR_PCI_ADDRESS) + #: The driver that the kernel should bind this device to for DPDK to use it. + os_driver_for_dpdk: str = Field(examples=["vfio-pci", "mlx5_core"]) + #: The operating system driver name when the operating system controls the port. + os_driver: str = Field(examples=["i40e", "ice", "mlx5_core"]) + #: The name of the peer node this port is connected to. + peer_node: str + #: The PCI address of the peer port connected to this port. + peer_pci: str = Field(pattern=REGEX_FOR_PCI_ADDRESS) + + +class TrafficGeneratorConfig(FrozenModel): + """A protocol required to define traffic generator types.""" + + #: The traffic generator type the child class is required to define to be distinguished among + #: others. + type: TrafficGeneratorType + + +class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): + """Scapy traffic generator specific configuration.""" + + type: Literal[TrafficGeneratorType.SCAPY] + + +#: A union type discriminating traffic generators by the `type` field. +TrafficGeneratorConfigTypes = Annotated[ScapyTrafficGeneratorConfig, Field(discriminator="type")] + +#: Comma-separated list of logical cores to use. An empty string or ```any``` means use all lcores. +LogicalCores = Annotated[ + str, + Field( + examples=["1,2,3,4,5,18-22", "10-15", "any"], + pattern=r"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$|any", + ), +] + + +class NodeConfiguration(FrozenModel): + r"""The configuration of :class:`~framework.testbed_model.node.Node`\s.""" + + #: The name of the :class:`~framework.testbed_model.node.Node`. + name: str + #: The hostname of the :class:`~framework.testbed_model.node.Node`. Can also be an IP address. + hostname: str + #: The name of the user used to connect to the :class:`~framework.testbed_model.node.Node`. + user: str + #: The password of the user. The use of passwords is heavily discouraged, please use SSH keys. + password: str | None = None + #: The operating system of the :class:`~framework.testbed_model.node.Node`. + os: OS + #: An optional hugepage configuration. + hugepages: HugepageConfiguration | None = Field(None, alias="hugepages_2mb") + #: The ports that can be used in testing. + ports: list[PortConfig] = Field(min_length=1) + + +class DPDKConfiguration(FrozenModel): + """Configuration of the DPDK EAL parameters.""" + + #: A comma delimited list of logical cores to use when running DPDK. ```any```, an empty + #: string or omitting this field means use any core except for the first one. The first core + #: will only be used if explicitly set. + lcores: LogicalCores = "" + + #: The number of memory channels to use when running DPDK. + memory_channels: int = 1 + + @property + def use_first_core(self) -> bool: + """Returns :data:`True` if `lcores` explicitly selects the first core.""" + return "0" in self.lcores + + +class SutNodeConfiguration(NodeConfiguration): + """:class:`~framework.testbed_model.sut_node.SutNode` specific configuration.""" + + #: The runtime configuration for DPDK. + dpdk_config: DPDKConfiguration + + +class TGNodeConfiguration(NodeConfiguration): + """:class:`~framework.testbed_model.tg_node.TGNode` specific configuration.""" + + #: The configuration of the traffic generator present on the TG node. + traffic_generator: TrafficGeneratorConfigTypes + + +#: Union type for all the node configuration types. +NodeConfigurationTypes = TGNodeConfiguration | SutNodeConfiguration diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py new file mode 100644 index 0000000000..f08e034e25 --- /dev/null +++ b/dts/framework/config/test_run.py @@ -0,0 +1,290 @@ +# SPDX-License-Identifier: BSD-3-Clause +# 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 + +"""Configuration models representing a test run. + +The root model of a test run configuration is :class:`TestRunConfiguration`. +""" + +import tarfile +from enum import auto, unique +from functools import cached_property +from pathlib import Path, PurePath +from typing import Any, Literal + +from pydantic import Field, field_validator, model_validator +from typing_extensions import TYPE_CHECKING, Self + +from framework.utils import StrEnum + +from .common import FrozenModel, load_from_settings + +if TYPE_CHECKING: + from framework.test_suite import TestSuiteSpec + + +@unique +class Compiler(StrEnum): + r"""The supported compilers of :class:`~framework.testbed_model.node.Node`\s.""" + + #: + gcc = auto() + #: + clang = auto() + #: + icc = auto() + #: + msvc = auto() + + +def resolve_path(path: Path) -> Path: + """Resolve a path into a real path.""" + return path.resolve() + + +class BaseDPDKLocation(FrozenModel): + """DPDK location base class. + + The path to the DPDK sources and type of location. + """ + + #: Specifies whether to find DPDK on the SUT node or on the local host. Which are respectively + #: represented by :class:`RemoteDPDKLocation` and :class:`LocalDPDKTreeLocation`. + remote: bool = False + + +class LocalDPDKLocation(BaseDPDKLocation): + """Local DPDK location base class. + + This class is meant to represent any location that is present only locally. + """ + + remote: Literal[False] = False + + +class LocalDPDKTreeLocation(LocalDPDKLocation): + """Local DPDK tree location. + + This class makes a distinction from :class:`RemoteDPDKTreeLocation` by enforcing on the fly + validation. + """ + + #: The path to the DPDK source tree directory on the local host passed as string. + dpdk_tree: Path + + #: Resolve the local DPDK tree path. + resolve_dpdk_tree_path = field_validator("dpdk_tree")(resolve_path) + + @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 + + +class LocalDPDKTarballLocation(LocalDPDKLocation): + """Local DPDK tarball location. + + This class makes a distinction from :class:`RemoteDPDKTarballLocation` by enforcing on the fly + validation. + """ + + #: The path to the DPDK tarball on the local host passed as string. + tarball: Path + + #: Resolve the local tarball path. + resolve_tarball_path = field_validator("tarball")(resolve_path) + + @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): + """Remote DPDK location base class. + + This class is meant to represent any location that is present only remotely. + """ + + remote: Literal[True] = True + + +class RemoteDPDKTreeLocation(RemoteDPDKLocation): + """Remote DPDK tree location. + + This class is distinct from :class:`LocalDPDKTreeLocation` which enforces on the fly validation. + """ + + #: The path to the DPDK source tree directory on the remote node passed as string. + dpdk_tree: PurePath + + +class RemoteDPDKTarballLocation(RemoteDPDKLocation): + """Remote DPDK tarball location. + + This class is distinct from :class:`LocalDPDKTarballLocation` which enforces on the fly + validation. + """ + + #: The path to the DPDK tarball on the remote node passed as string. + tarball: PurePath + + +#: Union type for different DPDK locations. +DPDKLocation = ( + LocalDPDKTreeLocation + | LocalDPDKTarballLocation + | RemoteDPDKTreeLocation + | RemoteDPDKTarballLocation +) + + +class BaseDPDKBuildConfiguration(FrozenModel): + """The base configuration for different types of build. + + The configuration contain the location of the DPDK and configuration used for building it. + """ + + #: The location of the DPDK tree. + dpdk_location: DPDKLocation + + dpdk_location_from_settings = field_validator("dpdk_location", mode="before")( + load_from_settings + ) + + +class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): + """DPDK precompiled build configuration.""" + + #: If it's defined, DPDK has been pre-compiled and the build directory is located in a + #: subdirectory of `~dpdk_location.dpdk_tree` or `~dpdk_location.tarball` root directory. + precompiled_build_dir: str = Field(min_length=1) + + build_dir_from_settings = field_validator("precompiled_build_dir", mode="before")( + load_from_settings + ) + + +class DPDKBuildOptionsConfiguration(FrozenModel): + """DPDK build options configuration. + + The build options used for building DPDK. + """ + + #: The compiler executable to use. + compiler: Compiler + #: This string will be put in front of the compiler when executing the build. Useful for adding + #: wrapper commands, such as ``ccache``. + compiler_wrapper: str = "" + + +class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration): + """DPDK uncompiled build configuration.""" + + #: The build options to compiled DPDK with. + build_options: DPDKBuildOptionsConfiguration + + +#: Union type for different build configurations. +DPDKBuildConfiguration = DPDKPrecompiledBuildConfiguration | DPDKUncompiledBuildConfiguration + + +class TestSuiteConfig(FrozenModel): + """Test suite configuration. + + 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 + """ + + #: The name of the test suite module without the starting ``TestSuite_``. + test_suite_name: str = Field(alias="test_suite") + #: The names of test cases from this test suite to execute. If empty, all test cases will be + #: executed. + test_cases_names: list[str] = Field(default_factory=list, 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 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}." + ) + + return self + + +class TestRunConfiguration(FrozenModel): + """The configuration of a test run. + + The configuration contains testbed information, what tests to execute + and with what DPDK build. + """ + + #: The DPDK configuration used to test. + dpdk_config: DPDKBuildConfiguration = Field(alias="dpdk_build") + #: Whether to run performance tests. + perf: bool + #: Whether to run functional tests. + func: bool + #: Whether to skip smoke tests. + skip_smoke_tests: bool = False + #: The names of test suites and/or test cases to execute. + test_suites: list[TestSuiteConfig] = Field(min_length=1) + #: The SUT node name to use in this test run. + system_under_test_node: str + #: The TG node name to use in this test run. + traffic_generator_node: str + #: The names of virtual devices to test. + vdevs: list[str] = Field(default_factory=list) + #: The seed to use for pseudo-random generation. + random_seed: int | None = None + + fields_from_settings = field_validator("test_suites", "random_seed", mode="before")( + load_from_settings + ) diff --git a/dts/framework/runner.py b/dts/framework/runner.py index e46a8c1a4f..9f9789cf49 100644 --- a/dts/framework/runner.py +++ b/dts/framework/runner.py @@ -25,17 +25,22 @@ from types import MethodType from typing import Iterable +from framework.config.common import ValidationContext from framework.testbed_model.capability import Capability, get_supported_capabilities from framework.testbed_model.sut_node import SutNode from framework.testbed_model.tg_node import TGNode from .config import ( Configuration, + load_config, +) +from .config.node import ( SutNodeConfiguration, + TGNodeConfiguration, +) +from .config.test_run import ( TestRunConfiguration, TestSuiteConfig, - TGNodeConfiguration, - load_config, ) from .exception import BlockingTestSuiteError, SSHTimeoutError, TestCaseVerifyError from .logger import DTSLogger, DtsStage, get_dts_logger @@ -81,7 +86,7 @@ class DTSRunner: def __init__(self): """Initialize the instance with configuration, logger, result and string constants.""" - self._configuration = load_config(SETTINGS) + self._configuration = load_config(ValidationContext(settings=SETTINGS)) self._logger = get_dts_logger() if not os.path.exists(SETTINGS.output_dir): os.makedirs(SETTINGS.output_dir) diff --git a/dts/framework/settings.py b/dts/framework/settings.py index 873d400bec..cf82a7c18f 100644 --- a/dts/framework/settings.py +++ b/dts/framework/settings.py @@ -14,10 +14,15 @@ The command line arguments along with the supported environment variables are: -.. option:: --config-file -.. envvar:: DTS_CFG_FILE +.. option:: --test-runs-config-file +.. envvar:: DTS_TEST_RUNS_CFG_FILE - The path to the YAML test run configuration file. + The path to the YAML configuration file of the test runs. + +.. option:: --nodes-config-file +.. envvar:: DTS_NODES_CFG_FILE + + The path to the YAML configuration file of the nodes. .. option:: --output-dir, --output .. envvar:: DTS_OUTPUT_DIR @@ -102,7 +107,7 @@ from pydantic import ValidationError -from .config import ( +from .config.test_run import ( DPDKLocation, LocalDPDKTarballLocation, LocalDPDKTreeLocation, @@ -120,7 +125,9 @@ class Settings: """ #: - config_file_path: Path = Path(__file__).parent.parent.joinpath("conf.yaml") + test_runs_config_path: Path = Path(__file__).parent.parent.joinpath("test_runs.yaml") + #: + nodes_config_path: Path = Path(__file__).parent.parent.joinpath("nodes.yaml") #: output_dir: str = "output" #: @@ -316,14 +323,24 @@ def _get_parser() -> _DTSArgumentParser: ) action = parser.add_argument( - "--config-file", - default=SETTINGS.config_file_path, + "--test-runs-config-file", + default=SETTINGS.test_runs_config_path, + type=Path, + help="The configuration file that describes the test cases and DPDK build options.", + metavar="FILE_PATH", + dest="test_runs_config_path", + ) + _add_env_var_to_action(action, "TEST_RUNS_CFG_FILE") + + action = parser.add_argument( + "--nodes-config-file", + default=SETTINGS.nodes_config_path, type=Path, - help="The configuration file that describes the test cases, SUTs and DPDK build configs.", + help="The configuration file that describes the SUT and TG nodes.", metavar="FILE_PATH", - dest="config_file_path", + dest="nodes_config_path", ) - _add_env_var_to_action(action, "CFG_FILE") + _add_env_var_to_action(action, "NODES_CFG_FILE") action = parser.add_argument( "--output-dir", diff --git a/dts/framework/test_result.py b/dts/framework/test_result.py index 381f72b974..9977a9c457 100644 --- a/dts/framework/test_result.py +++ b/dts/framework/test_result.py @@ -32,7 +32,7 @@ from framework.testbed_model.capability import Capability -from .config import TestRunConfiguration, TestSuiteConfig +from .config.test_run import TestRunConfiguration, TestSuiteConfig from .exception import DTSError, ErrorSeverity from .logger import DTSLogger from .test_suite import TestCase, TestSuite diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index 6c2dfd6185..e53a321499 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -15,10 +15,12 @@ from abc import ABC -from framework.config import ( +from framework.config.node import ( OS, - DPDKBuildConfiguration, NodeConfiguration, +) +from framework.config.test_run import ( + DPDKBuildConfiguration, TestRunConfiguration, ) from framework.exception import ConfigurationError diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py index e436886692..6d5fce40ff 100644 --- a/dts/framework/testbed_model/os_session.py +++ b/dts/framework/testbed_model/os_session.py @@ -28,7 +28,7 @@ from dataclasses import dataclass from pathlib import Path, PurePath, PurePosixPath -from framework.config import NodeConfiguration +from framework.config.node import NodeConfiguration from framework.logger import DTSLogger from framework.remote_session import ( InteractiveRemoteSession, diff --git a/dts/framework/testbed_model/port.py b/dts/framework/testbed_model/port.py index 566f4c5b46..7177da3371 100644 --- a/dts/framework/testbed_model/port.py +++ b/dts/framework/testbed_model/port.py @@ -10,7 +10,7 @@ from dataclasses import dataclass -from framework.config import PortConfig +from framework.config.node import PortConfig @dataclass(slots=True, frozen=True) diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py index d8f1f9d452..483733cede 100644 --- a/dts/framework/testbed_model/sut_node.py +++ b/dts/framework/testbed_model/sut_node.py @@ -16,7 +16,10 @@ from dataclasses import dataclass from pathlib import Path, PurePath -from framework.config import ( +from framework.config.node import ( + SutNodeConfiguration, +) +from framework.config.test_run import ( DPDKBuildConfiguration, DPDKBuildOptionsConfiguration, DPDKPrecompiledBuildConfiguration, @@ -25,7 +28,6 @@ LocalDPDKTreeLocation, RemoteDPDKTarballLocation, RemoteDPDKTreeLocation, - SutNodeConfiguration, TestRunConfiguration, ) from framework.exception import ConfigurationError, RemoteFileNotFoundError diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py index 3071bbd645..86cd278efb 100644 --- a/dts/framework/testbed_model/tg_node.py +++ b/dts/framework/testbed_model/tg_node.py @@ -11,7 +11,7 @@ from scapy.packet import Packet -from framework.config import TGNodeConfiguration +from framework.config.node import TGNodeConfiguration from framework.testbed_model.traffic_generator.capturing_traffic_generator import ( PacketFilteringConfig, ) diff --git a/dts/framework/testbed_model/topology.py b/dts/framework/testbed_model/topology.py index 0bad59d2a4..caee9b22ea 100644 --- a/dts/framework/testbed_model/topology.py +++ b/dts/framework/testbed_model/topology.py @@ -16,7 +16,7 @@ else: from aenum import NoAliasEnum -from framework.config import PortConfig +from framework.config.node import PortConfig from framework.exception import ConfigurationError from .port import Port diff --git a/dts/framework/testbed_model/traffic_generator/__init__.py b/dts/framework/testbed_model/traffic_generator/__init__.py index e501f6d5ee..922875f401 100644 --- a/dts/framework/testbed_model/traffic_generator/__init__.py +++ b/dts/framework/testbed_model/traffic_generator/__init__.py @@ -14,7 +14,7 @@ and a capturing traffic generator is required. """ -from framework.config import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig +from framework.config.node import ScapyTrafficGeneratorConfig, TrafficGeneratorConfig from framework.exception import ConfigurationError from framework.testbed_model.node import Node diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py index d24efc44e6..7b65d31723 100644 --- a/dts/framework/testbed_model/traffic_generator/scapy.py +++ b/dts/framework/testbed_model/traffic_generator/scapy.py @@ -20,7 +20,7 @@ from scapy.layers.l2 import Ether from scapy.packet import Packet -from framework.config import OS, ScapyTrafficGeneratorConfig +from framework.config.node import OS, ScapyTrafficGeneratorConfig from framework.remote_session.python_shell import PythonShell from framework.testbed_model.node import Node from framework.testbed_model.port import Port diff --git a/dts/framework/testbed_model/traffic_generator/traffic_generator.py b/dts/framework/testbed_model/traffic_generator/traffic_generator.py index a07538cc98..9b4d5dc80a 100644 --- a/dts/framework/testbed_model/traffic_generator/traffic_generator.py +++ b/dts/framework/testbed_model/traffic_generator/traffic_generator.py @@ -12,7 +12,7 @@ from scapy.packet import Packet -from framework.config import TrafficGeneratorConfig +from framework.config.node import TrafficGeneratorConfig from framework.logger import DTSLogger, get_dts_logger from framework.testbed_model.node import Node from framework.testbed_model.port import Port diff --git a/dts/nodes.example.yaml b/dts/nodes.example.yaml new file mode 100644 index 0000000000..454d97ab5d --- /dev/null +++ b/dts/nodes.example.yaml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2022-2023 The DPDK contributors +# Copyright 2023 Arm Limited + +# Define a system under test node, having two network ports physically +# connected to the corresponding ports in TG 1 (the peer node) +- name: "SUT 1" + hostname: sut1.change.me.localhost + user: dtsuser + os: linux + ports: + # sets up the physical link between "SUT 1"@0000:00:08.0 and "TG 1"@0000:00:08.0 + - pci: "0000:00:08.0" + os_driver_for_dpdk: vfio-pci # OS driver that DPDK will use + os_driver: i40e # OS driver to bind when the tests are not running + peer_node: "TG 1" + peer_pci: "0000:00:08.0" + # sets up the physical link between "SUT 1"@0000:00:08.1 and "TG 1"@0000:00:08.1 + - pci: "0000:00:08.1" + os_driver_for_dpdk: vfio-pci + os_driver: i40e + peer_node: "TG 1" + peer_pci: "0000:00:08.1" + hugepages_2mb: # optional; if removed, will use system hugepage configuration + number_of: 256 + force_first_numa: false + dpdk_config: + lcores: "" # use all available logical cores (Skips first core) + memory_channels: 4 # tells DPDK to use 4 memory channels +# Define a Scapy traffic generator node, having two network ports +# physically connected to the corresponding ports in SUT 1 (the peer node). +- name: "TG 1" + hostname: tg1.change.me.localhost + user: dtsuser + os: linux + ports: + # sets up the physical link between "TG 1"@0000:00:08.0 and "SUT 1"@0000:00:08.0 + - pci: "0000:00:08.0" + os_driver_for_dpdk: rdma + os_driver: rdma + peer_node: "SUT 1" + peer_pci: "0000:00:08.0" + # sets up the physical link between "SUT 1"@0000:00:08.0 and "TG 1"@0000:00:08.0 + - pci: "0000:00:08.1" + os_driver_for_dpdk: rdma + os_driver: rdma + peer_node: "SUT 1" + peer_pci: "0000:00:08.1" + hugepages_2mb: # optional; if removed, will use system hugepage configuration + number_of: 256 + force_first_numa: false + traffic_generator: + type: SCAPY diff --git a/dts/test_runs.example.yaml b/dts/test_runs.example.yaml new file mode 100644 index 0000000000..5b6afb153e --- /dev/null +++ b/dts/test_runs.example.yaml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2022-2023 The DPDK contributors +# Copyright 2023 Arm Limited + +# Define one test run environment +- dpdk_build: + 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: + # the combination of the following two makes CC="ccache gcc" + compiler: gcc + compiler_wrapper: ccache # Optional. + # If `precompiled_build_dir` is defined, DPDK has been pre-built and the build directory is + # in a subdirectory of DPDK tree root directory. Otherwise, will be using the `build_options` + # to build the DPDK from source. Either `precompiled_build_dir` or `build_options` can be + # defined, but not both. + perf: false # disable performance testing + func: true # enable functional testing + skip_smoke_tests: false # optional + test_suites: # the following test suites will be run in their entirety + - hello_world + vdevs: # optional; if removed, vdevs won't be used in the execution + - "crypto_openssl" + # The machine running the DPDK test executable + system_under_test_node: "SUT 1" + # Traffic generator node to use for this execution environment + traffic_generator_node: "TG 1" \ No newline at end of file diff --git a/dts/tests/TestSuite_smoke_tests.py b/dts/tests/TestSuite_smoke_tests.py index bc3a2a6bf9..5b50c0477b 100644 --- a/dts/tests/TestSuite_smoke_tests.py +++ b/dts/tests/TestSuite_smoke_tests.py @@ -14,7 +14,7 @@ import re -from framework.config import PortConfig +from framework.config.node import PortConfig from framework.remote_session.testpmd_shell import TestPmdShell from framework.settings import SETTINGS from framework.test_suite import TestSuite, func_test From patchwork Wed Jan 15 14:18:09 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luca Vizzarro X-Patchwork-Id: 150111 X-Patchwork-Delegate: paul.szczepanek@arm.com Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 97B924608B; Wed, 15 Jan 2025 15:20:20 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id A3B1D4065C; Wed, 15 Jan 2025 15:19:44 +0100 (CET) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mails.dpdk.org (Postfix) with ESMTP id 5F5BF4042F for ; Wed, 15 Jan 2025 15:19:40 +0100 (CET) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 4E2AB12FC; Wed, 15 Jan 2025 06:20:08 -0800 (PST) Received: from localhost.localdomain (JR4XG4HTQC.cambridge.arm.com [10.1.39.16]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 2C2523F63F; Wed, 15 Jan 2025 06:19:39 -0800 (PST) From: Luca Vizzarro To: dev@dpdk.org Cc: Nicholas Pratte , Luca Vizzarro , Paul Szczepanek , Patrick Robb Subject: [PATCH v3 7/7] dts: run all test suites by default Date: Wed, 15 Jan 2025 14:18:09 +0000 Message-ID: <20250115141809.3898708-8-luca.vizzarro@arm.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20250115141809.3898708-1-luca.vizzarro@arm.com> References: <20240705171341.23894-2-npratte@iol.unh.edu> <20250115141809.3898708-1-luca.vizzarro@arm.com> MIME-Version: 1.0 X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org The configuration requires the user to explicitly set the requested test suites in the files. Sometimes we want to run all the test suites and don't want to manually specify all of them. It is therefore reasonable to change the default behaviour to automatically run all the available test suites if none are specified. Bugzilla ID: 1360 Signed-off-by: Luca Vizzarro Reviewed-by: Paul Szczepanek Reviewed-by: Dean Marx --- dts/framework/config/test_run.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py index f08e034e25..6ea05ceeba 100644 --- a/dts/framework/config/test_run.py +++ b/dts/framework/config/test_run.py @@ -259,6 +259,20 @@ def validate_names(self) -> Self: return self +def fetch_all_test_suites() -> list[TestSuiteConfig]: + """Returns all the available test suites as configuration objects. + + This function does not include the smoke tests. + """ + from framework.test_suite import AVAILABLE_TEST_SUITES + + return [ + TestSuiteConfig(test_suite=test_suite.name) + for test_suite in AVAILABLE_TEST_SUITES + if test_suite.name != "smoke_tests" + ] + + class TestRunConfiguration(FrozenModel): """The configuration of a test run. @@ -275,7 +289,7 @@ class TestRunConfiguration(FrozenModel): #: Whether to skip smoke tests. skip_smoke_tests: bool = False #: The names of test suites and/or test cases to execute. - test_suites: list[TestSuiteConfig] = Field(min_length=1) + test_suites: list[TestSuiteConfig] = Field(min_length=1, default_factory=fetch_all_test_suites) #: The SUT node name to use in this test run. system_under_test_node: str #: The TG node name to use in this test run.