From patchwork Thu Apr 20 09:31:05 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 126317 X-Patchwork-Delegate: thomas@monjalon.net 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 71E2F42995; Thu, 20 Apr 2023 11:51:13 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 77EA842B71; Thu, 20 Apr 2023 11:51:09 +0200 (CEST) Received: from mail-ed1-f47.google.com (mail-ed1-f47.google.com [209.85.208.47]) by mails.dpdk.org (Postfix) with ESMTP id 7AFEC40687 for ; Thu, 20 Apr 2023 11:51:07 +0200 (CEST) Received: by mail-ed1-f47.google.com with SMTP id 4fb4d7f45d1cf-504e232fe47so796729a12.2 for ; Thu, 20 Apr 2023 02:51:07 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1681984267; x=1684576267; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=SljLnKM0DtAuQyGOqGkQ+gc9H0n5N2eK7T0QmEzFKTw=; b=PnYMx/7jdIbPMu3LInG7yYCAO8Da0pbVM34AIZnTQGQDqQViro8klzg0svF5HV6Nuz ZW/4WCAvbpiINL8/F9gxM6SzjT8/qDys/h4w9GDykszFQ37OPT9xSTkAg0P/yA7gbg5o U063pXy7xVyOeI1xGdQTQ45mKsdsOKkkKU6gs+h6CUlfuyiqvfBGIRiTtLmRsuNcvbaN iHpLOxIndCvNDWjDsYD7et3rcBJyiPNZ6v5+cRN15el+p14T3j5s5j9FM8QI3XUEBUXJ Z/zjSeLZZwRuOdTQGYswIZ/tOHplY/fH5o4uHhNjT0M+oDePSwojzdwXsuqLXrGKEV0+ T//w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1681984267; x=1684576267; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=SljLnKM0DtAuQyGOqGkQ+gc9H0n5N2eK7T0QmEzFKTw=; b=WxtS82MVmqfWT8tI9MVxuWRtK0dSmiwDsHsYD48jjrehnpxIX2m4lFJFVKitMuwGVi KEHtEoDs5+zlo8+1nqpwYKDx4CYUa0A5o+lzxS0HWOGHtY5uR38lC4TaeVVJCWYDcXk7 lhp+0c0zM9ldRvPC5Jl4hZ1NE4RVkDufBYr/5yK/zBN8y2s7NVA9ozfXp+Ov1W+RPKVu x/RZCG9HqmcQyd4bCI5JVv3tsItngUxA7gHwMVPykLyqbqUQQQKv8TkooiGtAdjwqFr2 fu+bDEM+GmIwR4M93UzzCHUVNAZxUOsD6FCI/DNpE4r9jJAfF21zwebyg0qAz6l18nq9 PuUg== X-Gm-Message-State: AAQBX9cCfl69KxxglLDCqU+A3bDZLj4oYXx6CFs5Ktu5dzrenvSQGszt rLfpVVFJP7wLKDb9gBeC8XulHtK4ofjzZFZ+ZIak9gmmVWVQSxjtUGl08ivzwT30JdXJqOCdaQ= = X-Google-Smtp-Source: AKy350aj8Nn/89LnsB8Tj/u+qATZiiUM1Vd86ip1mWcCk+8PqI4VVS7C1BL048UG3EkueSJq70hmdw== X-Received: by 2002:aa7:d914:0:b0:508:4123:1e53 with SMTP id a20-20020aa7d914000000b0050841231e53mr1038330edr.7.1681984267102; Thu, 20 Apr 2023 02:51:07 -0700 (PDT) Received: from jlinkes.pantheon.local (81.89.53.154.host.vnet.sk. [81.89.53.154]) by smtp.gmail.com with ESMTPSA id v2-20020aa7d802000000b004ad601533a3sm580801edq.55.2023.04.20.02.51.06 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 20 Apr 2023 02:51:06 -0700 (PDT) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, jspewock@iol.unh.edu, probb@iol.unh.edu Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 1/5] dts: add scapy dependency Date: Thu, 20 Apr 2023 11:31:05 +0200 Message-Id: <20230420093109.594704-2-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230420093109.594704-1-juraj.linkes@pantheon.tech> References: <20230420093109.594704-1-juraj.linkes@pantheon.tech> 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 Required for scapy traffic generator. Signed-off-by: Juraj Linkeš --- dts/poetry.lock | 18 +++++++++++++++++- dts/pyproject.toml | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dts/poetry.lock b/dts/poetry.lock index 64d6c18f35..4b6c42e280 100644 --- a/dts/poetry.lock +++ b/dts/poetry.lock @@ -425,6 +425,22 @@ files = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +[[package]] +name = "scapy" +version = "2.5.0" +description = "Scapy: interactive packet manipulation tool" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +files = [ + {file = "scapy-2.5.0.tar.gz", hash = "sha256:5b260c2b754fd8d409ba83ee7aee294ecdbb2c235f9f78fe90bc11cb6e5debc2"}, +] + +[package.extras] +basic = ["ipython"] +complete = ["cryptography (>=2.0)", "ipython", "matplotlib", "pyx"] +docs = ["sphinx (>=3.0.0)", "sphinx_rtd_theme (>=0.4.3)", "tox (>=3.0.0)"] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -504,4 +520,4 @@ jsonschema = ">=4,<5" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "af71d1ffeb4372d870bd02a8bf101577254e03ddb8b12d02ab174f80069fd853" +content-hash = "fba5dcbb12d55a9c6b3f59f062d3a40973ff8360edb023b6e4613522654ba7c1" diff --git a/dts/pyproject.toml b/dts/pyproject.toml index 72d5b0204d..fc9b6278bb 100644 --- a/dts/pyproject.toml +++ b/dts/pyproject.toml @@ -23,6 +23,7 @@ pexpect = "^4.8.0" warlock = "^2.0.1" PyYAML = "^6.0" types-PyYAML = "^6.0.8" +scapy = "^2.5.0" [tool.poetry.group.dev.dependencies] mypy = "^0.961" From patchwork Thu Apr 20 09:31:06 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 126318 X-Patchwork-Delegate: thomas@monjalon.net 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 3289442995; Thu, 20 Apr 2023 11:51:19 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 9327342C76; Thu, 20 Apr 2023 11:51:10 +0200 (CEST) Received: from mail-ej1-f44.google.com (mail-ej1-f44.google.com [209.85.218.44]) by mails.dpdk.org (Postfix) with ESMTP id 72C9B40EE3 for ; Thu, 20 Apr 2023 11:51:08 +0200 (CEST) Received: by mail-ej1-f44.google.com with SMTP id a640c23a62f3a-94a34d3812dso59246666b.3 for ; Thu, 20 Apr 2023 02:51:08 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1681984268; x=1684576268; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=qZto+FhOCTbkf7MCEx+fHxuEVFXw5qOIqWEGXHqqNlA=; b=zHHPpkSw8JWfx6FQKflklf18gP0A0idI5Im/NL6xPurtsooqHuqKRmgVOp1ogpnYEk lrNb9zN/mw5mwXk6jMpOSYV1otq/IJV5HFS8PDmUeL5emoqja9h+xuDB5ScH79AY2oYf M3UgCQ/mRSJvvP2MwBeP6BtFyBdFlhB5B0BeEK3Hk2RWuLztFPbHJiReFMT2hPdPy3wT aVXP+/t57TT7Jv5W3S4IWu/kX3Bhv7eLJbWwep3Svvn3nnnM3ZzubK/EJ73GqFIPxAbd 6TIOw++ZZFsvop70kmzEfxnYl/56zVN8r31vL9g1XSPihH8sTci5u2pOgQMAHCgP+RDz wn0g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1681984268; x=1684576268; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=qZto+FhOCTbkf7MCEx+fHxuEVFXw5qOIqWEGXHqqNlA=; b=UBxcwQGs728VqyZ7rM8EWgY3pkMs4oO1q1YfbYgtyCjxzxznGWAPBGA1/QZU66BoFH hUyoIaQQ7bQBdWRXeilWC78SOTUQ6+vkjk6VC+Gd99YPUt27YvwUjvtUCklfHe8NYA0p KxBPH+USx5gcY6tQ2/C6hOyIAkl6C4H9DHVX0BotsF9yyssgkWLJaNrfwOs3MvQfHHfH mfcyhNUJ9CGxRfe5tw5FzZb1R/cYPA4Q20TFqKVp5b6J6Cc1pX2sYNEWJCuWV8LNmIrQ iXm2jk8VwxzvK9Im1zxYPsz+IWUYIkMNaLE104Xlgqb8x52KhpWyan5e8GqvdmL0AFuM 8D2w== X-Gm-Message-State: AAQBX9c+IsGiVRuTosVzBAhJivO9oXcucCEdeCZ2U1LxkVDb7zyJXE1c CSudZKaDpSHEBAJWDaIrNt1PzMh4CZeHbUyX0xQtevflrKfwzKuEv6i25bBVXOEuJmsJ8tDi2w= = X-Google-Smtp-Source: AKy350bRP+iqP3w9o3q6WNdnG45KPurfqU/tqr/s9iHJcOyyPz+8vC9cMDseENx7J7wAPiEV1kwFrQ== X-Received: by 2002:aa7:dc1a:0:b0:505:abcd:d2cc with SMTP id b26-20020aa7dc1a000000b00505abcdd2ccmr1299878edu.30.1681984267962; Thu, 20 Apr 2023 02:51:07 -0700 (PDT) Received: from jlinkes.pantheon.local (81.89.53.154.host.vnet.sk. [81.89.53.154]) by smtp.gmail.com with ESMTPSA id v2-20020aa7d802000000b004ad601533a3sm580801edq.55.2023.04.20.02.51.07 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 20 Apr 2023 02:51:07 -0700 (PDT) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, jspewock@iol.unh.edu, probb@iol.unh.edu Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 2/5] dts: add traffic generator config Date: Thu, 20 Apr 2023 11:31:06 +0200 Message-Id: <20230420093109.594704-3-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230420093109.594704-1-juraj.linkes@pantheon.tech> References: <20230420093109.594704-1-juraj.linkes@pantheon.tech> 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 Node configuration - where to connect, what ports to use and what TG to use. Signed-off-by: Juraj Linkeš --- dts/conf.yaml | 25 +++ dts/framework/config/__init__.py | 107 +++++++++++-- dts/framework/config/conf_yaml_schema.json | 172 ++++++++++++++++++++- 3 files changed, 287 insertions(+), 17 deletions(-) diff --git a/dts/conf.yaml b/dts/conf.yaml index a9bd8a3ecf..4e5fd3560f 100644 --- a/dts/conf.yaml +++ b/dts/conf.yaml @@ -13,6 +13,7 @@ executions: test_suites: - hello_world system_under_test: "SUT 1" + traffic_generator_system: "TG 1" nodes: - name: "SUT 1" hostname: sut1.change.me.localhost @@ -25,3 +26,27 @@ nodes: hugepages: # optional; if removed, will use system hugepage configuration amount: 256 force_first_numa: false + ports: + - pci: "0000:00:08.0" + dpdk_os_driver: vfio-pci + os_driver: i40e + peer_node: "TG 1" + peer_pci: "0000:00:08.0" + - name: "TG 1" + hostname: tg1.change.me.localhost + user: root + arch: x86_64 + os: linux + lcores: "" + use_first_core: false + hugepages: # optional; if removed, will use system hugepage configuration + amount: 256 + force_first_numa: false + ports: + - pci: "0000:00:08.0" + dpdk_os_driver: rdma + os_driver: rdma + peer_node: "SUT 1" + peer_pci: "0000:00:08.0" + traffic_generator: + type: SCAPY diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py index ebb0823ff5..6b1c3159f7 100644 --- a/dts/framework/config/__init__.py +++ b/dts/framework/config/__init__.py @@ -12,7 +12,7 @@ import pathlib from dataclasses import dataclass from enum import Enum, auto, unique -from typing import Any, TypedDict +from typing import Any, TypedDict, Union import warlock # type: ignore import yaml @@ -61,6 +61,18 @@ class Compiler(StrEnum): msvc = auto() +@unique +class NodeType(StrEnum): + physical = auto() + virtual = auto() + + +@unique +class TrafficGeneratorType(StrEnum): + NONE = auto() + SCAPY = auto() + + # Slots enables some optimizations, by pre-allocating space for the defined # attributes in the underlying data structure. # @@ -72,6 +84,41 @@ class HugepageConfiguration: force_first_numa: bool +@dataclass(slots=True, frozen=True) +class PortConfig: + id: int + node: str + pci: str + dpdk_os_driver: str + os_driver: str + peer_node: str + peer_pci: str + + @staticmethod + def from_dict(id: int, node: str, d: dict) -> "PortConfig": + return PortConfig(id=id, node=node, **d) + + +@dataclass(slots=True, frozen=True) +class TrafficGeneratorConfig: + traffic_generator_type: TrafficGeneratorType + + @staticmethod + def from_dict(d: dict): + # This looks useless now, but is designed to allow expansion to traffic + # generators that require more configuration later. + match TrafficGeneratorType(d["type"]): + case TrafficGeneratorType.SCAPY: + return ScapyTrafficGeneratorConfig( + traffic_generator_type=TrafficGeneratorType.SCAPY + ) + + +@dataclass(slots=True, frozen=True) +class ScapyTrafficGeneratorConfig(TrafficGeneratorConfig): + pass + + @dataclass(slots=True, frozen=True) class NodeConfiguration: name: str @@ -82,29 +129,52 @@ class NodeConfiguration: os: OS lcores: str use_first_core: bool - memory_channels: int hugepages: HugepageConfiguration | None + ports: list[PortConfig] @staticmethod - def from_dict(d: dict) -> "NodeConfiguration": + def from_dict(d: dict) -> Union["SUTConfiguration", "TGConfiguration"]: hugepage_config = d.get("hugepages") if hugepage_config: if "force_first_numa" not in hugepage_config: hugepage_config["force_first_numa"] = False hugepage_config = HugepageConfiguration(**hugepage_config) - return NodeConfiguration( - name=d["name"], - hostname=d["hostname"], - user=d["user"], - password=d.get("password"), - arch=Architecture(d["arch"]), - os=OS(d["os"]), - lcores=d.get("lcores", "1"), - use_first_core=d.get("use_first_core", False), - memory_channels=d.get("memory_channels", 1), - hugepages=hugepage_config, - ) + common_config = {"name": d["name"], + "hostname": d["hostname"], + "user": d["user"], + "password": d.get("password"), + "arch": Architecture(d["arch"]), + "os": OS(d["os"]), + "lcores": d.get("lcores", "1"), + "use_first_core": d.get("use_first_core", False), + "hugepages": hugepage_config, + "ports": [ + PortConfig.from_dict(i, d["name"], port) + for i, port in enumerate(d["ports"]) + ]} + + if "traffic_generator" in d: + return TGConfiguration( + traffic_generator=TrafficGeneratorConfig.from_dict( + d["traffic_generator"]), + **common_config + ) + else: + return SUTConfiguration( + memory_channels=d.get("memory_channels", 1), + **common_config + ) + + +@dataclass(slots=True, frozen=True) +class SUTConfiguration(NodeConfiguration): + memory_channels: int + + +@dataclass(slots=True, frozen=True) +class TGConfiguration(NodeConfiguration): + traffic_generator: TrafficGeneratorConfig @dataclass(slots=True, frozen=True) @@ -156,7 +226,8 @@ class ExecutionConfiguration: perf: bool func: bool test_suites: list[TestSuiteConfig] - system_under_test: NodeConfiguration + system_under_test: SUTConfiguration + traffic_generator_system: TGConfiguration @staticmethod def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration": @@ -169,12 +240,16 @@ def from_dict(d: dict, node_map: dict) -> "ExecutionConfiguration": sut_name = d["system_under_test"] assert sut_name in node_map, f"Unknown SUT {sut_name} in execution {d}" + tg_name = d["traffic_generator_system"] + assert tg_name in node_map, f"Unknown TG {tg_name} in execution {d}" + return ExecutionConfiguration( build_targets=build_targets, perf=d["perf"], func=d["func"], test_suites=test_suites, system_under_test=node_map[sut_name], + traffic_generator_system=node_map[tg_name], ) diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json index ca2d4a1ef2..af1d071368 100644 --- a/dts/framework/config/conf_yaml_schema.json +++ b/dts/framework/config/conf_yaml_schema.json @@ -6,6 +6,76 @@ "type": "string", "description": "A unique identifier for a node" }, + "NIC": { + "type": "string", + "enum": [ + "ALL", + "ConnectX3_MT4103", + "ConnectX4_LX_MT4117", + "ConnectX4_MT4115", + "ConnectX5_MT4119", + "ConnectX5_MT4121", + "I40E_10G-10G_BASE_T_BC", + "I40E_10G-10G_BASE_T_X722", + "I40E_10G-SFP_X722", + "I40E_10G-SFP_XL710", + "I40E_10G-X722_A0", + "I40E_1G-1G_BASE_T_X722", + "I40E_25G-25G_SFP28", + "I40E_40G-QSFP_A", + "I40E_40G-QSFP_B", + "IAVF-ADAPTIVE_VF", + "IAVF-VF", + "IAVF_10G-X722_VF", + "ICE_100G-E810C_QSFP", + "ICE_25G-E810C_SFP", + "ICE_25G-E810_XXV_SFP", + "IGB-I350_VF", + "IGB_1G-82540EM", + "IGB_1G-82545EM_COPPER", + "IGB_1G-82571EB_COPPER", + "IGB_1G-82574L", + "IGB_1G-82576", + "IGB_1G-82576_QUAD_COPPER", + "IGB_1G-82576_QUAD_COPPER_ET2", + "IGB_1G-82580_COPPER", + "IGB_1G-I210_COPPER", + "IGB_1G-I350_COPPER", + "IGB_1G-I354_SGMII", + "IGB_1G-PCH_LPTLP_I218_LM", + "IGB_1G-PCH_LPTLP_I218_V", + "IGB_1G-PCH_LPT_I217_LM", + "IGB_1G-PCH_LPT_I217_V", + "IGB_2.5G-I354_BACKPLANE_2_5GBPS", + "IGC-I225_LM", + "IGC-I226_LM", + "IXGBE_10G-82599_SFP", + "IXGBE_10G-82599_SFP_SF_QP", + "IXGBE_10G-82599_T3_LOM", + "IXGBE_10G-82599_VF", + "IXGBE_10G-X540T", + "IXGBE_10G-X540_VF", + "IXGBE_10G-X550EM_A_SFP", + "IXGBE_10G-X550EM_X_10G_T", + "IXGBE_10G-X550EM_X_SFP", + "IXGBE_10G-X550EM_X_VF", + "IXGBE_10G-X550T", + "IXGBE_10G-X550_VF", + "brcm_57414", + "brcm_P2100G", + "cavium_0011", + "cavium_a034", + "cavium_a063", + "cavium_a064", + "fastlinq_ql41000", + "fastlinq_ql41000_vf", + "fastlinq_ql45000", + "fastlinq_ql45000_vf", + "hi1822", + "virtio" + ] + }, + "ARCH": { "type": "string", "enum": [ @@ -20,6 +90,20 @@ "linux" ] }, + "OS_WITH_OPTIONS": { + "oneOf": [ + { + "$ref": "#/definitions/OS" + }, + { + "type": "string", + "enum": [ + "ALL", + "OTHER" + ] + } + ] + }, "cpu": { "type": "string", "description": "Native should be the default on x86", @@ -94,6 +178,34 @@ "amount" ] }, + "mac_address": { + "type": "string", + "description": "A MAC address", + "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" + }, + "pktgen_type": { + "type": "string", + "enum": [ + "IXIA", + "IXIA_NETWORK", + "TREX", + "SCAPY", + "NONE" + ] + }, + "pci_address": { + "type": "string", + "pattern": "^[\\da-fA-F]{4}:[\\da-fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$" + }, + "port_peer_address": { + "description": "Peer is a TRex port, and IXIA port or a PCI address", + "oneOf": [ + { + "description": "PCI peer port", + "$ref": "#/definitions/pci_address" + } + ] + }, "test_suite": { "type": "string", "enum": [ @@ -165,6 +277,60 @@ }, "hugepages": { "$ref": "#/definitions/hugepages" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "description": "Each port should be described on both sides of the connection. This makes configuration slightly more verbose but greatly simplifies implementation. If there are an inconsistencies, then DTS will not run until that issue is fixed. An example inconsistency would be port 1, node 1 says it is connected to port 1, node 2, but port 1, node 2 says it is connected to port 2, node 1.", + "properties": { + "pci": { + "$ref": "#/definitions/pci_address", + "description": "The local PCI address of the port" + }, + "dpdk_os_driver": { + "type": "string", + "description": "The driver that the kernel should bind this device to for DPDK to use it. (ex: vfio-pci)" + }, + "os_driver": { + "type": "string", + "description": "The driver normally used by this port (ex: i40e)" + }, + "peer_node": { + "type": "string", + "description": "The name of the node the peer port is on" + }, + "peer_pci": { + "$ref": "#/definitions/pci_address", + "description": "The PCI address of the peer port" + } + }, + "additionalProperties": false, + "required": [ + "pci", + "dpdk_os_driver", + "os_driver", + "peer_node", + "peer_pci" + ] + }, + "minimum": 1 + }, + "traffic_generator": { + "oneOf": [ + { + "type": "object", + "description": "Scapy traffic generator", + "properties": { + "type": { + "type": "string", + "enum": [ + "SCAPY" + ] + } + } + } + ] } }, "additionalProperties": false, @@ -213,6 +379,9 @@ }, "system_under_test": { "$ref": "#/definitions/node_name" + }, + "traffic_generator_system": { + "$ref": "#/definitions/node_name" } }, "additionalProperties": false, @@ -221,7 +390,8 @@ "perf", "func", "test_suites", - "system_under_test" + "system_under_test", + "traffic_generator_system" ] }, "minimum": 1 From patchwork Thu Apr 20 09:31:07 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 126319 X-Patchwork-Delegate: thomas@monjalon.net 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 F011B42995; Thu, 20 Apr 2023 11:51:25 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id B490042D0E; Thu, 20 Apr 2023 11:51:11 +0200 (CEST) Received: from mail-ed1-f51.google.com (mail-ed1-f51.google.com [209.85.208.51]) by mails.dpdk.org (Postfix) with ESMTP id 4CF6741141 for ; Thu, 20 Apr 2023 11:51:09 +0200 (CEST) Received: by mail-ed1-f51.google.com with SMTP id 4fb4d7f45d1cf-504eb1155d3so3304932a12.1 for ; Thu, 20 Apr 2023 02:51:09 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1681984269; x=1684576269; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=WasRetr7H9jzTixrqLWkJJR4SmZAe7m2uEzwpvlGscc=; b=VySPb/LTgpEusI3/gezUi8/wMJYbNdgjbkztwfk16IGR3KYUp06ZrOmNRcaR5vksX1 2mva0dgYY+locCLhkzuSiMggeo8Y5wubVTAxB/2z9qkEQato/Hsal4G4ecY8C73L+bDJ 0Y8KL/qXSyEDeDxTOfAKFMGjIIKSs/oJCsDqE5QyCvJxSTAmTU4vjJC2GBm1Qg5c985N RW/G/6yde8Stwgfa/KCP+HMEfYbG06rgCUq0/b6tcklJfVMEG0fKufp8M8tkEbXgqBy5 fyGLxtMCHkksTczEcuoGOUbZa6kKAya2FBU2S6bAuxrX4tNoRVOf1d7staQR+wxLka6p N1aA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1681984269; x=1684576269; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=WasRetr7H9jzTixrqLWkJJR4SmZAe7m2uEzwpvlGscc=; b=apZEhDGeDcUFSkwcYc57yC5h8IqP32tuvFyeDhOzxla09oXR2xDZo0PdDSI036INQ5 ZQWI8EQuF2a0o2khGlyYwmbMGrvMG5Eup6yRLhwDVEqcwvQGIeCuR1JmRo4B6Sx7R66G BiQ9hujbcD6+wtOZ9nFOICVsX35P7eBa7mNvZJ5Ns/4By2mjLvos77hj8yyrWVS+sDxd xVi4ty8O8USUwew3Pmphj9FkZkC1XgFzNI1YoEIE2/4RvJcspupsF9DZUVIwQDgLwC4w nK5waN7Zh5nrhQ82aMURif2//sHkoydKpqUIYuQwssaJPAGGus3mvMHQJiALZkKirkMU LlXQ== X-Gm-Message-State: AAQBX9eCxsydQFgsrxVYmlhbIzq+xqQUbiuGZRSzkCEGDhNAFwDfChX/ D7kJQICMKr4HSZvjpHOBX6Yoxgq3Xdm7UhQE+TyjvtYxpS2Pliv8z481zzw71RuS9MB9dT/ikA= = X-Google-Smtp-Source: AKy350bAT8ISczk+n18WZ9ejq0A7DoB1xSt9Gk01lmIeoc4ANoL/J6ISvoTPJL2NUQ1hnVW2l26Sxw== X-Received: by 2002:a05:6402:711:b0:4fb:5fe1:bc3b with SMTP id w17-20020a056402071100b004fb5fe1bc3bmr969947edx.0.1681984268847; Thu, 20 Apr 2023 02:51:08 -0700 (PDT) Received: from jlinkes.pantheon.local (81.89.53.154.host.vnet.sk. [81.89.53.154]) by smtp.gmail.com with ESMTPSA id v2-20020aa7d802000000b004ad601533a3sm580801edq.55.2023.04.20.02.51.08 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 20 Apr 2023 02:51:08 -0700 (PDT) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, jspewock@iol.unh.edu, probb@iol.unh.edu Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 3/5] dts: traffic generator abstractions Date: Thu, 20 Apr 2023 11:31:07 +0200 Message-Id: <20230420093109.594704-4-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230420093109.594704-1-juraj.linkes@pantheon.tech> References: <20230420093109.594704-1-juraj.linkes@pantheon.tech> 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 There are traffic abstractions for all traffic generators and for traffic generators that can capture (not just count) packets. There also related abstractions, such as TGNode where the traffic generators reside and some related code. Signed-off-by: Juraj Linkeš --- dts/framework/remote_session/os_session.py | 22 ++- dts/framework/remote_session/posix_session.py | 3 + .../capturing_traffic_generator.py | 155 ++++++++++++++++++ dts/framework/testbed_model/hw/port.py | 55 +++++++ dts/framework/testbed_model/node.py | 4 +- dts/framework/testbed_model/sut_node.py | 5 +- dts/framework/testbed_model/tg_node.py | 62 +++++++ .../testbed_model/traffic_generator.py | 59 +++++++ 8 files changed, 360 insertions(+), 5 deletions(-) create mode 100644 dts/framework/testbed_model/capturing_traffic_generator.py create mode 100644 dts/framework/testbed_model/hw/port.py create mode 100644 dts/framework/testbed_model/tg_node.py create mode 100644 dts/framework/testbed_model/traffic_generator.py diff --git a/dts/framework/remote_session/os_session.py b/dts/framework/remote_session/os_session.py index 4c48ae2567..56d7fef06c 100644 --- a/dts/framework/remote_session/os_session.py +++ b/dts/framework/remote_session/os_session.py @@ -10,6 +10,7 @@ from framework.logger import DTSLOG from framework.settings import SETTINGS from framework.testbed_model import LogicalCore +from framework.testbed_model.hw.port import PortIdentifier from framework.utils import EnvVarsDict, MesonArgs from .remote import CommandResult, RemoteSession, create_remote_session @@ -37,6 +38,7 @@ def __init__( self.name = name self._logger = logger self.remote_session = create_remote_session(node_config, name, logger) + self._disable_terminal_colors() def close(self, force: bool = False) -> None: """ @@ -53,7 +55,7 @@ def is_alive(self) -> bool: def send_command( self, command: str, - timeout: float, + timeout: float = SETTINGS.timeout, verify: bool = False, env: EnvVarsDict | None = None, ) -> CommandResult: @@ -64,6 +66,12 @@ def send_command( """ return self.remote_session.send_command(command, timeout, verify, env) + @abstractmethod + def _disable_terminal_colors(self) -> None: + """ + Disable the colors in the ssh session. + """ + @abstractmethod def guess_dpdk_remote_dir(self, remote_dir) -> PurePath: """ @@ -173,3 +181,15 @@ def setup_hugepages(self, hugepage_amount: int, force_first_numa: bool) -> None: if needed and mount the hugepages if needed. If force_first_numa is True, configure hugepages just on the first socket. """ + + @abstractmethod + def get_logical_name_of_port(self, id: PortIdentifier) -> str | None: + """ + Gets the logical name (eno1, ens5, etc) of a port by the port's identifier. + """ + + @abstractmethod + def check_link_is_up(self, id: PortIdentifier) -> bool: + """ + Check that the link is up. + """ diff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/remote_session/posix_session.py index d38062e8d6..288fbabf1e 100644 --- a/dts/framework/remote_session/posix_session.py +++ b/dts/framework/remote_session/posix_session.py @@ -219,3 +219,6 @@ def _remove_dpdk_runtime_dirs( def get_dpdk_file_prefix(self, dpdk_prefix) -> str: return "" + + def _disable_terminal_colors(self) -> None: + self.remote_session.send_command("export TERM=xterm-mono") diff --git a/dts/framework/testbed_model/capturing_traffic_generator.py b/dts/framework/testbed_model/capturing_traffic_generator.py new file mode 100644 index 0000000000..7beeb139c1 --- /dev/null +++ b/dts/framework/testbed_model/capturing_traffic_generator.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2022 University of New Hampshire +# + +import itertools +import uuid +from abc import abstractmethod + +import scapy.utils +from scapy.packet import Packet + +from framework.testbed_model.hw.port import PortIdentifier +from framework.settings import SETTINGS + +from .traffic_generator import TrafficGenerator + + +def _get_default_capture_name() -> str: + """ + This is the function used for the default implementation of capture names. + """ + return str(uuid.uuid4()) + + +class CapturingTrafficGenerator(TrafficGenerator): + """ + A mixin interface which enables a packet generator to declare that it can capture + packets and return them to the user. + + All packet functions added by this class should write out the captured packets + to a pcap file in output, allowing for easier analysis of failed tests. + """ + + def is_capturing(self) -> bool: + return True + + @abstractmethod + def send_packet_and_capture( + self, + send_port_id: PortIdentifier, + packet: Packet, + receive_port_id: PortIdentifier, + duration_s: int, + capture_name: str = _get_default_capture_name(), + ) -> list[Packet]: + """ + Send a packet on the send port and then capture all traffic on receive port + for the given duration. + + Captures packets and adds them to output/.pcap. + + This function must handle no packets even being received. + """ + raise NotImplementedError() + + def send_packets_and_capture( + self, + send_port_id: PortIdentifier, + packets: list[Packet], + receive_port_id: PortIdentifier, + duration_s: int, + capture_name: str = _get_default_capture_name(), + ) -> list[Packet]: + """ + Send a group of packets on the send port and then capture all traffic on the + receive port for the given duration. + + This function must handle no packets even being received. + """ + self.logger.info( + f"Incremental captures will be created at output/{capture_name}-.pcap" + ) + received_packets: list[list[Packet]] = [] + for i, packet in enumerate(packets): + received_packets.append( + self.send_packet_and_capture( + send_port_id, + packet, + receive_port_id, + duration_s, + capture_name=f"{capture_name}-{i}", + ) + ) + + flattened_received_packets = list( + itertools.chain.from_iterable(received_packets) + ) + scapy.utils.wrpcap(f"output/{capture_name}.pcap", flattened_received_packets) + return flattened_received_packets + + def send_packet_and_expect_packet( + self, + send_port_id: PortIdentifier, + packet: Packet, + receive_port_id: PortIdentifier, + expected_packet: Packet, + timeout: int = SETTINGS.timeout, + capture_name: str = _get_default_capture_name(), + ) -> None: + """ + Sends the provided packet, capturing received packets. Then asserts that the + only 1 packet was received, and that the packet that was received is equal to + the expected packet. + """ + packets: list[Packet] = self.send_packet_and_capture( + send_port_id, packet, receive_port_id, timeout + ) + + assert len(packets) != 0, "Expected a packet, but none were captured" + assert len(packets) == 1, ( + "More packets than expected were received, " + f"capture written to output/{capture_name}.pcap" + ) + assert packets[0] == expected_packet, ( + f"Received packet differed from expected packet, capture written to " + f"output/{capture_name}.pcap" + ) + + def send_packets_and_expect_packets( + self, + send_port_id: PortIdentifier, + packets: list[Packet], + receive_port_id: PortIdentifier, + expected_packets: list[Packet], + timeout: int = SETTINGS.timeout, + capture_name: str = _get_default_capture_name(), + ) -> None: + """ + Sends the provided packets, capturing received packets. Then asserts that the + correct number of packets was received, and that the packets that were received + are equal to the expected packet. This equality is done by comparing packets + at the same index. + """ + packets: list[Packet] = self.send_packets_and_capture( + send_port_id, packets, receive_port_id, timeout + ) + + if len(expected_packets) > 0: + assert len(packets) != 0, "Expected packets, but none were captured" + + assert len(packets) == len(expected_packets), ( + "A different number of packets than expected were received, " + f"capture written to output/{capture_name}.pcap or split across " + f"output/{capture_name}-.pcap" + ) + for i, expected_packet in enumerate(expected_packets): + assert packets[i] == expected_packet, ( + f"Received packet {i} differed from expected packet, capture written " + f"to output/{capture_name}.pcap or output/{capture_name}-{i}.pcap" + ) + + def _write_capture_from_packets(self, capture_name: str, packets: list[Packet]): + file_name = f"output/{capture_name}.pcap" + self.logger.debug(f"Writing packets to {file_name}") + scapy.utils.wrpcap(file_name, packets) diff --git a/dts/framework/testbed_model/hw/port.py b/dts/framework/testbed_model/hw/port.py new file mode 100644 index 0000000000..ebaad563f8 --- /dev/null +++ b/dts/framework/testbed_model/hw/port.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2022 University of New Hampshire +# + +from dataclasses import dataclass + +from framework.config import PortConfig + + +@dataclass(slots=True, frozen=True) +class PortIdentifier: + node: str + pci: str + + +@dataclass(slots=True, frozen=True) +class Port: + """ + identifier: The information that uniquely identifies this port. + pci: The PCI address of the port. + + os_driver: The driver normally used by this port (ex: i40e) + dpdk_os_driver: The driver that the os should bind this device to for DPDK to use it. (ex: vfio-pci) + + Note: os_driver and dpdk_os_driver may be the same thing, see mlx5_core + + peer: The identifier for whatever this port is plugged into. + """ + + id: int + identifier: PortIdentifier + os_driver: str + dpdk_os_driver: str + peer: PortIdentifier + + @property + def node(self) -> str: + return self.identifier.node + + @property + def pci(self) -> str: + return self.identifier.pci + + @staticmethod + def from_config(node_name: str, config: PortConfig) -> "Port": + return Port( + id=config.id, + identifier=PortIdentifier( + node=node_name, + pci=config.pci, + ), + os_driver=config.os_driver, + dpdk_os_driver=config.dpdk_os_driver, + peer=PortIdentifier(node=config.peer_node, pci=config.peer_pci), + ) diff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py index d48fafe65d..5d2d1a0cf6 100644 --- a/dts/framework/testbed_model/node.py +++ b/dts/framework/testbed_model/node.py @@ -47,6 +47,8 @@ def __init__(self, node_config: NodeConfiguration): self._logger = getLogger(self.name) self.main_session = create_session(self.config, self.name, self._logger) + self._logger.info(f"Connected to node: {self.name}") + self._get_remote_cpus() # filter the node lcores according to user config self.lcores = LogicalCoreListFilter( @@ -55,8 +57,6 @@ def __init__(self, node_config: NodeConfiguration): self._other_sessions = [] - self._logger.info(f"Created node: {self.name}") - def set_up_execution(self, execution_config: ExecutionConfiguration) -> None: """ Perform the execution setup that will be done for each execution diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py index 2b2b50d982..ec9180a98b 100644 --- a/dts/framework/testbed_model/sut_node.py +++ b/dts/framework/testbed_model/sut_node.py @@ -7,7 +7,7 @@ import time from pathlib import PurePath -from framework.config import BuildTargetConfiguration, NodeConfiguration +from framework.config import BuildTargetConfiguration, SUTConfiguration from framework.remote_session import CommandResult, OSSession from framework.settings import SETTINGS from framework.utils import EnvVarsDict, MesonArgs @@ -34,7 +34,7 @@ class SutNode(Node): _app_compile_timeout: float _dpdk_kill_session: OSSession | None - def __init__(self, node_config: NodeConfiguration): + def __init__(self, node_config: SUTConfiguration): super(SutNode, self).__init__(node_config) self._dpdk_prefix_list = [] self._build_target_config = None @@ -47,6 +47,7 @@ def __init__(self, node_config: NodeConfiguration): self._dpdk_timestamp = ( f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}" ) + self._logger.info(f"Created node: {self.name}") @property def _remote_dpdk_dir(self) -> PurePath: diff --git a/dts/framework/testbed_model/tg_node.py b/dts/framework/testbed_model/tg_node.py new file mode 100644 index 0000000000..fdb7329020 --- /dev/null +++ b/dts/framework/testbed_model/tg_node.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2010-2014 Intel Corporation +# Copyright(c) 2022 University of New Hampshire +# Copyright(c) 2023 PANTHEON.tech s.r.o. + +from scapy.layers.inet import IP, UDP +from scapy.layers.l2 import Ether +from scapy.packet import Raw + +from framework.config import TGConfiguration +from framework.testbed_model.hw.port import Port, PortIdentifier +from framework.testbed_model.traffic_generator import TrafficGenerator + +from .node import Node + + +class TGNode(Node): + """ + A class for managing connections to the Traffic Generator node and managing + traffic generators residing within. + """ + + ports: list[Port] + traffic_generator: TrafficGenerator + + def __init__(self, node_config: TGConfiguration): + super(TGNode, self).__init__(node_config) + self.ports = [ + Port.from_config(self.name, port_config) + for port_config in node_config.ports + ] + self.traffic_generator = TrafficGenerator.from_config( + self, node_config.traffic_generator + ) + self._logger.info(f"Created node: {self.name}") + + def get_ports_with_loop_topology( + self, peer_node: "Node" + ) -> tuple[PortIdentifier, PortIdentifier] | None: + for port1 in self.ports: + if port1.peer.node == peer_node.name: + for port2 in self.ports: + if port2.peer.node == peer_node.name: + return (port1.identifier, port2.identifier) + self._logger.warning( + f"Attempted to find loop topology between {self.name} and {peer_node.name}, but none could be found" + ) + return None + + def verify(self) -> None: + for port in self.ports: + self.traffic_generator.assert_port_is_connected(port.identifier) + port = self.ports[0] + # Check that the traffic generator is working by sending a packet. + # send_packet should throw an error if something goes wrong. + self.traffic_generator.send_packet( + port.identifier, Ether() / IP() / UDP() / Raw(b"Hello World") + ) + + def close(self) -> None: + self.traffic_generator.close() + super(TGNode, self).close() diff --git a/dts/framework/testbed_model/traffic_generator.py b/dts/framework/testbed_model/traffic_generator.py new file mode 100644 index 0000000000..ea8f361e8f --- /dev/null +++ b/dts/framework/testbed_model/traffic_generator.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2022 University of New Hampshire +# + +from abc import ABC, abstractmethod + +from scapy.packet import Packet + +from framework.config import TrafficGeneratorConfig, TrafficGeneratorType +from framework.logger import DTSLOG +from framework.testbed_model.hw.port import PortIdentifier + + +class TrafficGenerator(ABC): + logger: DTSLOG + + @abstractmethod + def send_packet(self, port: PortIdentifier, packet: Packet) -> None: + """ + Sends a packet and blocks until it is fully sent. + + What fully sent means is defined by the traffic generator. + """ + raise NotImplementedError() + + def send_packets(self, port: PortIdentifier, packets: list[Packet]) -> None: + """ + Sends a list of packets and blocks until they are fully sent. + + What "fully sent" means is defined by the traffic generator. + """ + # default implementation, this should be overridden if there is a better + # way to do this on a specific packet generator + for packet in packets: + self.send_packet(port, packet) + + def is_capturing(self) -> bool: + """ + Whether this traffic generator can capture traffic + """ + return False + + @staticmethod + def from_config( + node: "Node", traffic_generator_config: TrafficGeneratorConfig + ) -> "TrafficGenerator": + from .scapy import ScapyTrafficGenerator + + match traffic_generator_config.traffic_generator_type: + case TrafficGeneratorType.SCAPY: + return ScapyTrafficGenerator(node, node.ports) + + @abstractmethod + def close(self): + pass + + @abstractmethod + def assert_port_is_connected(self, id: PortIdentifier) -> None: + pass From patchwork Thu Apr 20 09:31:08 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 126320 X-Patchwork-Delegate: thomas@monjalon.net 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 B3FA542995; Thu, 20 Apr 2023 11:51:32 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id C101642D16; Thu, 20 Apr 2023 11:51:12 +0200 (CEST) Received: from mail-ed1-f47.google.com (mail-ed1-f47.google.com [209.85.208.47]) by mails.dpdk.org (Postfix) with ESMTP id 2B37F42C24 for ; Thu, 20 Apr 2023 11:51:10 +0200 (CEST) Received: by mail-ed1-f47.google.com with SMTP id 4fb4d7f45d1cf-5050491cb04so676994a12.0 for ; Thu, 20 Apr 2023 02:51:10 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1681984270; x=1684576270; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=YUJOXbbpdckjOQaxjF8cg17k9E6EwTm+W94hVYVmA58=; b=zcFwpGDQAcfLjB+EmPvu85d6dIWsVCcH2r3C1SfIc6Ht2wXWSE1XzcPxmrTka4oNCp AN0rv4phKVy5eQ40e/odRlj/G+X+VBenXqWq25xkd+hkR4Su5Yg1jTKS2nhqIpqrSP7X ZZH8LvUfW7goh5dXmDSSMtaL1JcLWkHGBNe8+yfZGkuRPiTyJp8D6hFd9zDvc11EILV7 MySYP4YOO1BdQnKEqqdJ3RFwcbAqpFeyzwDdeIq8SXZrZbWvnlq4zAG6ALGvdFo2H4WD rvFPF/6ka32iDwgnmaLD7VIGln/IGzz9/YxB8tbwoakP/BM9odQFTSAnHrfnHAABUPX4 CuYg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1681984270; x=1684576270; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=YUJOXbbpdckjOQaxjF8cg17k9E6EwTm+W94hVYVmA58=; b=MSYNtLydjISgKeB0oH/0cjvto1z8xaBlWWXTuHk2dfIt0LwtUnU1fm0V39gyllLnQX rNji32CgBAUy/TsFC62vgkdSMABFG2mxrQBQYHJaGi9QFrM7ai1Oxp+cTStAJBrFULir BHhvdAhzyhdlglQgsxqBLQ+rcmUzAvK64+IpT4d5aqmh9RSxS2S0Is5SWcZqlygZZNz8 fwEawlQbZaLUM8IeG2faljH9ZACwrwSdBaQHSE2cwLzvZNhIvawLJPe7Qcsb3IhDTOpU s9QIpEQBl6v2Im7gz/PSmfhqdEYRFiVl8NzA6Z86bPoTYPSLHr976okZhsOYoPMsqaXx LpZw== X-Gm-Message-State: AAQBX9eJIMcKNhK4A1lLrNqXnJ1vOwzJuPx4gs7fwHIXfB8PuFG1e66G YbtV2xw3ibt2DrssaDtI4mDV7JT7Z72hMNehRJb8dxbvDLabanr+LhsAhbiR6CC4ldo4El7DzGQ zq0GzC3y4O6I= X-Google-Smtp-Source: AKy350YZw26MdHFyGpfKf+/ms5cREHXYTyO9fWAOAawM6skeWO9GK+iZNl6Mtg68/QYEwUysl3Aimw== X-Received: by 2002:aa7:c3c7:0:b0:506:7671:4e72 with SMTP id l7-20020aa7c3c7000000b0050676714e72mr1079174edr.38.1681984269716; Thu, 20 Apr 2023 02:51:09 -0700 (PDT) Received: from jlinkes.pantheon.local (81.89.53.154.host.vnet.sk. [81.89.53.154]) by smtp.gmail.com with ESMTPSA id v2-20020aa7d802000000b004ad601533a3sm580801edq.55.2023.04.20.02.51.08 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 20 Apr 2023 02:51:09 -0700 (PDT) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, jspewock@iol.unh.edu, probb@iol.unh.edu Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 4/5] dts: scapy traffic generator implementation Date: Thu, 20 Apr 2023 11:31:08 +0200 Message-Id: <20230420093109.594704-5-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230420093109.594704-1-juraj.linkes@pantheon.tech> References: <20230420093109.594704-1-juraj.linkes@pantheon.tech> 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 Scapy is a traffic generator capable of sending and receiving traffic. Since it's a software traffic generator, it's not suitable for performance testing, but it is suitable for functional testing. Signed-off-by: Juraj Linkeš --- dts/framework/remote_session/linux_session.py | 55 +++ .../remote_session/remote/remote_session.py | 7 + dts/framework/testbed_model/scapy.py | 348 ++++++++++++++++++ 3 files changed, 410 insertions(+) create mode 100644 dts/framework/testbed_model/scapy.py diff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/remote_session/linux_session.py index a1e3bc3a92..b99a27bba4 100644 --- a/dts/framework/remote_session/linux_session.py +++ b/dts/framework/remote_session/linux_session.py @@ -2,13 +2,29 @@ # Copyright(c) 2023 PANTHEON.tech s.r.o. # Copyright(c) 2023 University of New Hampshire +import json +from typing import TypedDict +from typing_extensions import NotRequired + + from framework.exception import RemoteCommandExecutionError from framework.testbed_model import LogicalCore +from framework.testbed_model.hw.port import PortIdentifier from framework.utils import expand_range from .posix_session import PosixSession +class LshwOutputConfigurationDict(TypedDict): + link: str + + +class LshwOutputDict(TypedDict): + businfo: str + logicalname: NotRequired[str] + configuration: LshwOutputConfigurationDict + + class LinuxSession(PosixSession): """ The implementation of non-Posix compliant parts of Linux remote sessions. @@ -105,3 +121,42 @@ def _configure_huge_pages( self.remote_session.send_command( f"echo {amount} | sudo tee {hugepage_config_path}" ) + + def get_lshw_info(self) -> list[LshwOutputDict]: + output = self.remote_session.send_expect("lshw -quiet -json -C network", "#") + assert not isinstance( + output, int + ), "send_expect returned an int when it should have been a string" + return json.loads(output) + + def get_logical_name_of_port(self, id: PortIdentifier) -> str | None: + self._logger.debug(f"Searching for logical name of {id.pci}") + assert ( + id.node == self.name + ), "Attempted to get the logical port name on the wrong node" + port_info_list: list[LshwOutputDict] = self.get_lshw_info() + for port_info in port_info_list: + if f"pci@{id.pci}" == port_info.get("businfo"): + if "logicalname" in port_info: + self._logger.debug( + f"Found logical name for port {id.pci}, {port_info.get('logicalname')}" + ) + return port_info.get("logicalname") + else: + self._logger.warning( + f"Attempted to get the logical name of {id.pci}, but none existed" + ) + return None + self._logger.warning(f"No port at pci address {id.pci} found.") + return None + + def check_link_is_up(self, id: PortIdentifier) -> bool | None: + self._logger.debug(f"Checking link status for {id.pci}") + port_info_list: list[LshwOutputDict] = self.get_lshw_info() + for port_info in port_info_list: + if f"pci@{id.pci}" == port_info.get("businfo"): + status = port_info["configuration"]["link"] + self._logger.debug(f"Found link status for port {id.pci}, {status}") + return status == "up" + self._logger.warning(f"No port at pci address {id.pci} found.") + return None diff --git a/dts/framework/remote_session/remote/remote_session.py b/dts/framework/remote_session/remote/remote_session.py index 91dee3cb4f..5b36e2d7d2 100644 --- a/dts/framework/remote_session/remote/remote_session.py +++ b/dts/framework/remote_session/remote/remote_session.py @@ -84,6 +84,13 @@ def _connect(self) -> None: Create connection to assigned node. """ + @abstractmethod + def send_expect( + self, command: str, prompt: str, timeout: float = 15, + verify: bool = False + ) -> str | int: + """""" + def send_command( self, command: str, diff --git a/dts/framework/testbed_model/scapy.py b/dts/framework/testbed_model/scapy.py new file mode 100644 index 0000000000..1e5caab897 --- /dev/null +++ b/dts/framework/testbed_model/scapy.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2022 University of New Hampshire +# + +import inspect +import json +import marshal +import types +import xmlrpc.client +from typing import TypedDict +from xmlrpc.server import SimpleXMLRPCServer + +import scapy.all +from scapy.packet import Packet +from typing_extensions import NotRequired + +from framework.config import OS +from framework.logger import getLogger +from .tg_node import TGNode +from .hw.port import Port, PortIdentifier +from .capturing_traffic_generator import ( + CapturingTrafficGenerator, + _get_default_capture_name, +) +from framework.settings import SETTINGS +from framework.remote_session import OSSession + +""" +========= BEGIN RPC FUNCTIONS ========= + +All of the functions in this section are intended to be exported to a python +shell which runs a scapy RPC server. These functions are made available via that +RPC server to the packet generator. To add a new function to the RPC server, +first write the function in this section. Then, if you need any imports, make sure to +add them to SCAPY_RPC_SERVER_IMPORTS as well. After that, add the function to the list +in EXPORTED_FUNCTIONS. Note that kwargs (keyword arguments) do not work via xmlrpc, +so you may need to construct wrapper functions around many scapy types. +""" + +""" +Add the line needed to import something in a normal python environment +as an entry to this array. It will be imported before any functions are +sent to the server. +""" +SCAPY_RPC_SERVER_IMPORTS = [ + "from scapy.all import *", + "import xmlrpc", + "import sys", + "from xmlrpc.server import SimpleXMLRPCServer", + "import marshal", + "import pickle", + "import types", +] + + +def scapy_sr1_different_interfaces( + packets: list[Packet], send_iface: str, recv_iface: str, timeout_s: int +) -> bytes: + packets = [scapy.all.Packet(packet.data) for packet in packets] + sniffer = scapy.all.AsyncSniffer( + iface=recv_iface, + store=True, + timeout=timeout_s, + started_callback=lambda _: scapy.all.sendp(packets, iface=send_iface), + stop_filter=lambda _: True, + ) + sniffer.start() + packets = sniffer.stop(join=True) + assert len(packets) != 0, "Not enough packets were sniffed" + assert len(packets) == 1, "More packets than expected were sniffed" + return packets[0].build() + + +def scapy_send_packets_and_capture( + packets: list[Packet], send_iface: str, recv_iface: str, duration_s: int +) -> list[bytes]: + packets = [scapy.all.Packet(packet.data) for packet in packets] + sniffer = scapy.all.AsyncSniffer( + iface=recv_iface, + store=True, + timeout=duration_s, + started_callback=lambda _: scapy.all.sendp(packets, iface=send_iface), + ) + sniffer.start() + return [packet.build() for packet in sniffer.stop(join=True)] + + +def scapy_send_packets(packets: list[xmlrpc.client.Binary], send_iface: str) -> None: + packets = [scapy.all.Packet(packet.data) for packet in packets] + scapy.all.sendp(packets, iface=send_iface, realtime=True, verbose=True) + + +""" +Functions to be exposed by the scapy RPC server. +""" +RPC_FUNCTIONS = [ + scapy_send_packets, + scapy_send_packets_and_capture, + scapy_sr1_different_interfaces, +] + + +class QuittableXMLRPCServer(SimpleXMLRPCServer): + def __init__(self, *args, **kwargs): + kwargs["allow_none"] = True + super().__init__(*args, **kwargs) + self.register_introspection_functions() + self.register_function(self.quit) + self.register_function(self.add_rpc_function) + + def quit(self) -> None: + self._BaseServer__shutdown_request = True + return None + + def add_rpc_function(self, name: str, function_bytes: xmlrpc.client.Binary): + function_code = marshal.loads(function_bytes.data) + function = types.FunctionType(function_code, globals(), name) + self.register_function(function) + + def serve_forever(self, poll_interval: float = 0.5) -> None: + print("XMLRPC OK") + super().serve_forever(poll_interval) + + +""" +========= END RPC FUNCTIONS ========= +""" + + +class NetworkInfoDict(TypedDict): + businfo: str + logicalname: NotRequired[str] + + +class ScapyTrafficGenerator(CapturingTrafficGenerator): + """ + Provides access to scapy functions via an RPC interface + """ + + tg_node: TGNode + ports: list[Port] + session: OSSession + scapy: xmlrpc.client.ServerProxy + iface_names: dict[PortIdentifier, str] + + def __init__(self, tg_node: TGNode, ports: list[Port]): + self.tg_node = tg_node + + assert tg_node.config.os == OS.linux, ( + "Linux is the only supported OS for scapy traffic generation" + ) + + self.session = tg_node.create_session("scapy") + self.logger = getLogger("scapy-pktgen-messages", node=tg_node.name) + self.ports = ports + + # No fancy colors + + prompt_str = "" + self.session.remote_session.send_expect(f'export PS1="{prompt_str}"', prompt_str) + + network_info_str: str = self.session.remote_session.send_expect( + "lshw -quiet -json -C network", prompt_str, timeout=10 + ) + + network_info_list: list[NetworkInfoDict] = json.loads(network_info_str) + network_info_lookup: dict[str, str] = { + network_info["businfo"]: network_info.get("logicalname") + for network_info in network_info_list + } + + self.iface_names = dict() + for port in self.ports: + businfo_str = f"pci@{port.pci}" + assert businfo_str in network_info_lookup, ( + f"Expected '{businfo_str}' in lshw output for {self.tg_node.name}, but " + f"it was not present." + ) + + self.iface_names[port.identifier] = network_info_lookup[businfo_str] + + assert ( + self.iface_names[port.identifier] is not None + ), f"No interface was present for {port.pci} on {self.tg_node.name}" + + self._run_command("python3") + + self._add_helper_functions_to_scapy() + self.session.remote_session.send_expect( + 'server = QuittableXMLRPCServer(("0.0.0.0", 8000)); server.serve_forever()', + "XMLRPC OK", + timeout=5, + ) + + server_url: str = f"http://{self.tg_node.config.hostname}:8000" + + self.scapy = xmlrpc.client.ServerProxy( + server_url, allow_none=True, verbose=SETTINGS.verbose + ) + + for function in RPC_FUNCTIONS: + # A slightly hacky way to move a function to the remote server. + # It is constructed from the name and code on the other side. + # Pickle cannot handle functions, nor can any of the other serialization + # frameworks aside from the libraries used to generate pyc files, which + # are even more messy to work with. + function_bytes = marshal.dumps(function.__code__) + self.scapy.add_rpc_function(function.__name__, function_bytes) + + def _add_helper_functions_to_scapy(self): + for import_statement in SCAPY_RPC_SERVER_IMPORTS: + self._run_command(import_statement + "\r\n") + + for helper_function in {QuittableXMLRPCServer}: + # load the source of the function + src = inspect.getsource(helper_function) + # Lines with only whitespace break the repl if in the middle of a function + # or class, so strip all lines containing only whitespace + src = "\n".join( + [line for line in src.splitlines() if not line.isspace() and line != ""] + ) + + spacing = "\n" * 4 + + # execute it in the python terminal + self._run_command(spacing + src + spacing) + + def _run_command(self, command: str) -> str: + return self.session.remote_session.send_expect(command, ">>>") + + def _get_port_interface_or_error(self, port: PortIdentifier) -> str: + match self.iface_names.get(port): + case None: + assert ( + False + ), f"{port} is not a valid port on this packet generator on {self.tg_node.name}." + case iface: + return iface + + def send_packet(self, port: PortIdentifier, packet: Packet) -> None: + iface = self._get_port_interface_or_error(port) + self.logger.info("Sending packet") + self.logger.debug("Packet contents: \n" + packet._do_summary()[1]) + self.scapy.scapy_send_packets([packet.build()], iface) + + def send_packets(self, port: PortIdentifier, packets: list[Packet]) -> None: + iface = self._get_port_interface_or_error(port) + self.logger.info("Sending packets") + packet_summaries = json.dumps( + list(map(lambda pkt: pkt._do_summary()[1], packets)), indent=4 + ) + packets = [packet.build() for packet in packets] + self.logger.debug("Packet contents: \n" + packet_summaries) + self.scapy.scapy_send_packets(packets, iface) + + def send_packet_and_capture( + self, + send_port_id: PortIdentifier, + packet: Packet, + receive_port_id: PortIdentifier, + duration_s: int, + capture_name: str = _get_default_capture_name(), + ) -> list[Packet]: + packets = self.scapy.scapy_send_packets_and_capture( + [packet.build()], send_port_id, receive_port_id, duration_s + ) + self._write_capture_from_packets(capture_name, packets) + return packets + + def send_packets_and_capture( + self, + send_port_id: PortIdentifier, + packets: Packet, + receive_port_id: PortIdentifier, + duration_s: int, + capture_name: str = _get_default_capture_name(), + ) -> list[Packet]: + packets: list[bytes] = [packet.build() for packet in packets] + packets: list[bytes] = self.scapy.scapy_send_packets_and_capture( + packets, send_port_id, receive_port_id, duration_s + ) + packets: list[Packet] = [scapy.all.Packet(packet) for packet in packets] + self._write_capture_from_packets(capture_name, packets) + return packets + + def send_packet_and_expect_packet( + self, + send_port_id: PortIdentifier, + packet: Packet, + receive_port_id: PortIdentifier, + expected_packet: Packet, + timeout: int = SETTINGS.timeout, + capture_name: str = _get_default_capture_name(), + ) -> None: + self.send_packets_and_expect_packets( + send_port_id, + [packet], + receive_port_id, + [expected_packet], + timeout, + capture_name, + ) + + def send_packets_and_expect_packets( + self, + send_port_id: PortIdentifier, + packets: list[Packet], + receive_port_id: PortIdentifier, + expected_packets: list[Packet], + timeout: int = SETTINGS.timeout, + capture_name: str = _get_default_capture_name(), + ) -> None: + send_iface = self._get_port_interface_or_error(send_port_id) + recv_iface = self._get_port_interface_or_error(receive_port_id) + + packets = [packet.build() for packet in packets] + + received_packets = self.scapy.scapy_sr1_different_interfaces( + packets, send_iface, recv_iface, timeout + ) + + received_packets = [scapy.all.Packet(packet) for packet in received_packets] + + self._write_capture_from_packets(capture_name, received_packets) + + assert len(received_packets) == len( + expected_packets + ), "Incorrect number of packets received" + for i, expected_packet in enumerate(expected_packets): + assert ( + received_packets[i] == expected_packet + ), f"Received packet {i} differed from expected packet" + + def close(self): + try: + self.scapy.quit() + except ConnectionRefusedError: + # Because the python instance closes, we get no RPC response. + # Thus, this error is expected + pass + try: + self.session.close(force=True) + except TimeoutError: + # Pexpect does not like being in a python prompt when it closes + pass + + def assert_port_is_connected(self, id: PortIdentifier) -> None: + self.tg_node.main_session.check_link_is_up(id) From patchwork Thu Apr 20 09:31:09 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Juraj_Linke=C5=A1?= X-Patchwork-Id: 126321 X-Patchwork-Delegate: thomas@monjalon.net 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 B2A4042995; Thu, 20 Apr 2023 11:51:39 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id D89D542D20; Thu, 20 Apr 2023 11:51:13 +0200 (CEST) Received: from mail-ej1-f45.google.com (mail-ej1-f45.google.com [209.85.218.45]) by mails.dpdk.org (Postfix) with ESMTP id EEE4342D0C for ; Thu, 20 Apr 2023 11:51:10 +0200 (CEST) Received: by mail-ej1-f45.google.com with SMTP id b16so5071345ejz.3 for ; Thu, 20 Apr 2023 02:51:10 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1681984270; x=1684576270; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=D4Lrv1vUwvnAgnztEFHIt45B2ITEsQJxVRI7Wx0YypU=; b=mxnY7SXgM/rvaX07KcVBuZqOkXCtMao17ftNnY8MhN7tn5OHM2S7LBxs9TCUnrRejZ H2s5s40u5WwVbJezjFmIEF64af8JFUwVAU3/NGexrKzSPejpPFev9iveE+D/PV14WuJo hOQBhcichVJ3X7kpT+FnKh2EqqCnQy4UB1K2Ri5xNUFFgx60MiQseDsQi0+C6DfG4xEi yM1gB2p1Xfo4KCbN63/m4/31UgLQNLdUu42i6SKNkp2ZyewjR4wuqya5hH4ORAhnOsOl 2YHa7XfNvCPn46RXs31WCOHekLbX2Mz+bF6KpnbmI2KruM46k+ViVHt0McK09NwPbuGI bnzg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1681984270; x=1684576270; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=D4Lrv1vUwvnAgnztEFHIt45B2ITEsQJxVRI7Wx0YypU=; b=UVhYYT42R75ZZjNRSLyqvYTNKkeaXTEOVkwJdW0NXnxjukREORKBsKZZcb6Vabg5dx +7D1QR+nyfuWLpmQT7Y0UlWANirJQaEph237A9PkAC0RtYNTGz6Q2EphIZtYp0KJ+UlW iz/oG9rBCxRieS4HLbHB3T704a0PCMGESg9Q9lxco3BlWcWBjEAkNVHOkh19CNTIsGRC yKtzjg29qi+81nn6G4pDykOD8quKsljp6za/V6QjaW8SdyC+hFdPCyETDCgjmgvIHAp6 jA1wVB/izi+/XSIdIvoWRJiBQiXg2LiIH8TqhOfhhba+eCLhx9Q0dUuBgX8ezNTVvKoz y6qw== X-Gm-Message-State: AAQBX9e33HfNAzTqirtb/UJ8wXRiW3wfROL/xaT5XWqFBaTQ59oCHs0Q Xh1M9YdXi3D7qbfbfjfZR3CKYaOLcF9u0stTJH5dmy6Tly40aYhabpmKA286o8woiQeeITKH0g= = X-Google-Smtp-Source: AKy350Z1IY1JWkErrLflQ8mob41xneNJvK3itCVfNNGhSjGcvfn7oN9DmeOz2/fbnor/H1fEbgliVw== X-Received: by 2002:a17:907:5c5:b0:94f:62a2:d1ab with SMTP id wg5-20020a17090705c500b0094f62a2d1abmr1253760ejb.63.1681984270551; Thu, 20 Apr 2023 02:51:10 -0700 (PDT) Received: from jlinkes.pantheon.local (81.89.53.154.host.vnet.sk. [81.89.53.154]) by smtp.gmail.com with ESMTPSA id v2-20020aa7d802000000b004ad601533a3sm580801edq.55.2023.04.20.02.51.09 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 20 Apr 2023 02:51:10 -0700 (PDT) From: =?utf-8?q?Juraj_Linke=C5=A1?= To: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, jspewock@iol.unh.edu, probb@iol.unh.edu Cc: dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= Subject: [RFC PATCH v1 5/5] dts: add traffic generator node to dts runner Date: Thu, 20 Apr 2023 11:31:09 +0200 Message-Id: <20230420093109.594704-6-juraj.linkes@pantheon.tech> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230420093109.594704-1-juraj.linkes@pantheon.tech> References: <20230420093109.594704-1-juraj.linkes@pantheon.tech> 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 Initialize the TG node and do basic verification. Signed-off-by: Juraj Linkeš Acked-by: Jeremy Spewock --- dts/framework/dts.py | 42 ++++++++++++++++--------- dts/framework/testbed_model/__init__.py | 1 + 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/dts/framework/dts.py b/dts/framework/dts.py index 0502284580..9c82bfe1f4 100644 --- a/dts/framework/dts.py +++ b/dts/framework/dts.py @@ -9,7 +9,7 @@ from .logger import DTSLOG, getLogger from .test_result import BuildTargetResult, DTSResult, ExecutionResult, Result from .test_suite import get_test_suites -from .testbed_model import SutNode +from .testbed_model import SutNode, TGNode, Node from .utils import check_dts_python_version dts_logger: DTSLOG = getLogger("DTSRunner") @@ -27,28 +27,40 @@ def run_all() -> None: # check the python version of the server that run dts check_dts_python_version() - nodes: dict[str, SutNode] = {} + nodes: dict[str, Node] = {} try: # for all Execution sections for execution in CONFIGURATION.executions: sut_node = None + tg_node = None if execution.system_under_test.name in nodes: # a Node with the same name already exists sut_node = nodes[execution.system_under_test.name] - else: - # the SUT has not been initialized yet - try: + + if execution.traffic_generator_system.name in nodes: + # a Node with the same name already exists + tg_node = nodes[execution.traffic_generator_system.name] + + try: + if not sut_node: sut_node = SutNode(execution.system_under_test) - result.update_setup(Result.PASS) - except Exception as e: - dts_logger.exception( - f"Connection to node {execution.system_under_test} failed." - ) - result.update_setup(Result.FAIL, e) - else: - nodes[sut_node.name] = sut_node - - if sut_node: + if not tg_node: + tg_node = TGNode(execution.traffic_generator_system) + tg_node.verify() + result.update_setup(Result.PASS) + except Exception as e: + failed_node = execution.system_under_test.name + if sut_node: + failed_node = execution.traffic_generator_system.name + dts_logger.exception( + f"Creation of node {failed_node} failed." + ) + result.update_setup(Result.FAIL, e) + else: + nodes[sut_node.name] = sut_node + nodes[tg_node.name] = tg_node + + if sut_node and tg_node: _run_execution(sut_node, execution, result) except Exception as e: diff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py index f54a947051..5cbb859e47 100644 --- a/dts/framework/testbed_model/__init__.py +++ b/dts/framework/testbed_model/__init__.py @@ -20,3 +20,4 @@ ) from .node import Node from .sut_node import SutNode +from .tg_node import TGNode