Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/129646/?format=api
http://patchwork.dpdk.org/api/patches/129646/?format=api", "web_url": "http://patchwork.dpdk.org/project/dpdk/patch/20230719141303.33284-7-juraj.linkes@pantheon.tech/", "project": { "id": 1, "url": "http://patchwork.dpdk.org/api/projects/1/?format=api", "name": "DPDK", "link_name": "dpdk", "list_id": "dev.dpdk.org", "list_email": "dev@dpdk.org", "web_url": "http://core.dpdk.org", "scm_url": "git://dpdk.org/dpdk", "webscm_url": "http://git.dpdk.org/dpdk", "list_archive_url": "https://inbox.dpdk.org/dev", "list_archive_url_format": "https://inbox.dpdk.org/dev/{}", "commit_url_format": "" }, "msgid": "<20230719141303.33284-7-juraj.linkes@pantheon.tech>", "list_archive_url": "https://inbox.dpdk.org/dev/20230719141303.33284-7-juraj.linkes@pantheon.tech", "date": "2023-07-19T14:13:03", "name": "[v3,6/6] dts: add basic UDP test case", "commit_ref": null, "pull_url": null, "state": "accepted", "archived": true, "hash": "597d998a06ea02120290b0778c2df738eeb4acdc", "submitter": { "id": 1626, "url": "http://patchwork.dpdk.org/api/people/1626/?format=api", "name": "Juraj Linkeš", "email": "juraj.linkes@pantheon.tech" }, "delegate": { "id": 1, "url": "http://patchwork.dpdk.org/api/users/1/?format=api", "username": "tmonjalo", "first_name": "Thomas", "last_name": "Monjalon", "email": "thomas@monjalon.net" }, "mbox": "http://patchwork.dpdk.org/project/dpdk/patch/20230719141303.33284-7-juraj.linkes@pantheon.tech/mbox/", "series": [ { "id": 28973, "url": "http://patchwork.dpdk.org/api/series/28973/?format=api", "web_url": "http://patchwork.dpdk.org/project/dpdk/list/?series=28973", "date": "2023-07-19T14:12:57", "name": "dts: tg abstractions and scapy tg", "version": 3, "mbox": "http://patchwork.dpdk.org/series/28973/mbox/" } ], "comments": "http://patchwork.dpdk.org/api/patches/129646/comments/", "check": "warning", "checks": "http://patchwork.dpdk.org/api/patches/129646/checks/", "tags": {}, "related": [], "headers": { "Return-Path": "<dev-bounces@dpdk.org>", "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])\n\tby inbox.dpdk.org (Postfix) with ESMTP id EFDCF42EB9;\n\tWed, 19 Jul 2023 16:13:47 +0200 (CEST)", "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id 8A52842D33;\n\tWed, 19 Jul 2023 16:13:14 +0200 (CEST)", "from mail-ed1-f53.google.com (mail-ed1-f53.google.com\n [209.85.208.53]) by mails.dpdk.org (Postfix) with ESMTP id 26FD042D1A\n for <dev@dpdk.org>; Wed, 19 Jul 2023 16:13:12 +0200 (CEST)", "by mail-ed1-f53.google.com with SMTP id\n 4fb4d7f45d1cf-51e5e4c6026so9856917a12.0\n for <dev@dpdk.org>; Wed, 19 Jul 2023 07:13:12 -0700 (PDT)", "from jlinkes-PT-Latitude-5530.. (ip-46.34.247.144.o2inet.sk.\n [46.34.247.144]) by smtp.gmail.com with ESMTPSA id\n q8-20020a056402040800b0051e2809395bsm2721979edv.63.2023.07.19.07.13.10\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Wed, 19 Jul 2023 07:13:11 -0700 (PDT)" ], "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=pantheon.tech; s=google; t=1689775992; x=1690380792;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=hbBQxh6qZUKii8+3FXzVySz2o7TnPCxR6Sn2yjaMZdI=;\n b=tfUajYQN6sob9oasohOuLTXR4ZJqP9x8kUDWyBIpxryBJg4Fni5y+AbP+MEhWDr9NQ\n YRGM38FKTT9TsG0x7uDXPgXMPPSxw8Mi6nPRfBoSSvx3be3cQktZoLqhtaYskvWSJACw\n ANbAalIya+tiKdAQpI0e46PNuyqquJkYyg0/amNiON9syzjoxg0Scs0HQsSJgXjAMlDT\n HaqsD7Hro765qXSOC/oBNi4qmQeXVG5jfG0pN4cqypvdv+3bLVLOvWBNyMc6Tlc5P2nK\n 54uZXEHs+3+YLdFkTuztR1GerNb9B6M5cc3DcsZAF2HNWcxGHrkugOOwM4Av/ntFsn8M\n 9Eqg==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20221208; t=1689775992; x=1690380792;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=hbBQxh6qZUKii8+3FXzVySz2o7TnPCxR6Sn2yjaMZdI=;\n b=MJnwXc/LEcpXxMchYxpxbbK65eOspo1XdhgXFKQK6+Yuxj3aF6k36SNLuEn7KOTbew\n bq7K4YUHI26ZLXZDRDyFgmrABgRCy2Z/7k3gdGsXxLnx3qLg68BRvRzVTZ2tcrhfK9IO\n y9zobI0gjC0GpJ9fJ1vX+6+OfKKdqEO6shqXiTl7SPo6hU0m4Q3xFUusg73Zo1d0c99Q\n 5KoXRf2MVqot2gErs1m82UzcFwnQbbXe7QssHQMpHKWwZchEWoYMvszyzjXSOCxoRrVk\n kFL8Jjj9G9LvDOiRqqBlFZHeSha0nPCnK4Kyey+DGN/kAQhTJj5COnJQPqYibAF2cTqH\n s2yQ==", "X-Gm-Message-State": "ABy/qLbiPmqItm5ZVFaSRNrCnFmdz5LRdPYbuYiXwS39EEuwVUVU0GBC\n Cic9vB1HBxSwYlapLg86yo7OGg==", "X-Google-Smtp-Source": "\n APBJJlHRYCqwazyA+z069RXCuC2RIwGo4tLN5yCScktIUL+bZcIRnHAq4yGIyPJyRxe08YXI/6cDqw==", "X-Received": "by 2002:a05:6402:517b:b0:51e:5169:6262 with SMTP id\n d27-20020a056402517b00b0051e51696262mr2629814ede.15.1689775991730;\n Wed, 19 Jul 2023 07:13:11 -0700 (PDT)", "From": "=?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>", "To": "thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com,\n bruce.richardson@intel.com, jspewock@iol.unh.edu, probb@iol.unh.edu", "Cc": "dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>", "Subject": "[PATCH v3 6/6] dts: add basic UDP test case", "Date": "Wed, 19 Jul 2023 16:13:03 +0200", "Message-Id": "<20230719141303.33284-7-juraj.linkes@pantheon.tech>", "X-Mailer": "git-send-email 2.34.1", "In-Reply-To": "<20230719141303.33284-1-juraj.linkes@pantheon.tech>", "References": "<20230717110709.39220-1-juraj.linkes@pantheon.tech>\n <20230719141303.33284-1-juraj.linkes@pantheon.tech>", "MIME-Version": "1.0", "Content-Type": "text/plain; charset=UTF-8", "Content-Transfer-Encoding": "8bit", "X-BeenThere": "dev@dpdk.org", "X-Mailman-Version": "2.1.29", "Precedence": "list", "List-Id": "DPDK patches and discussions <dev.dpdk.org>", "List-Unsubscribe": "<https://mails.dpdk.org/options/dev>,\n <mailto:dev-request@dpdk.org?subject=unsubscribe>", "List-Archive": "<http://mails.dpdk.org/archives/dev/>", "List-Post": "<mailto:dev@dpdk.org>", "List-Help": "<mailto:dev-request@dpdk.org?subject=help>", "List-Subscribe": "<https://mails.dpdk.org/listinfo/dev>,\n <mailto:dev-request@dpdk.org?subject=subscribe>", "Errors-To": "dev-bounces@dpdk.org" }, "content": "The test cases showcases the scapy traffic generator code.\n\nSigned-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>\n---\n dts/conf.yaml | 1 +\n dts/framework/config/conf_yaml_schema.json | 3 +-\n dts/framework/remote_session/linux_session.py | 20 +-\n dts/framework/remote_session/os_session.py | 20 +-\n dts/framework/test_suite.py | 217 +++++++++++++++++-\n dts/framework/testbed_model/node.py | 14 +-\n dts/framework/testbed_model/sut_node.py | 3 +\n dts/tests/TestSuite_os_udp.py | 45 ++++\n 8 files changed, 315 insertions(+), 8 deletions(-)\n create mode 100644 dts/tests/TestSuite_os_udp.py", "diff": "diff --git a/dts/conf.yaml b/dts/conf.yaml\nindex 0440d1d20a..37967daea0 100644\n--- a/dts/conf.yaml\n+++ b/dts/conf.yaml\n@@ -13,6 +13,7 @@ executions:\n skip_smoke_tests: false # optional flag that allows you to skip smoke tests\n test_suites:\n - hello_world\n+ - os_udp\n system_under_test_node:\n node_name: \"SUT 1\"\n vdevs: # optional; if removed, vdevs won't be used in the execution\ndiff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json\nindex 936a4bac5b..84e45fe3c2 100644\n--- a/dts/framework/config/conf_yaml_schema.json\n+++ b/dts/framework/config/conf_yaml_schema.json\n@@ -185,7 +185,8 @@\n \"test_suite\": {\n \"type\": \"string\",\n \"enum\": [\n- \"hello_world\"\n+ \"hello_world\",\n+ \"os_udp\"\n ]\n },\n \"test_target\": {\ndiff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/remote_session/linux_session.py\nindex decce4039c..a3f1a6bf3b 100644\n--- a/dts/framework/remote_session/linux_session.py\n+++ b/dts/framework/remote_session/linux_session.py\n@@ -3,7 +3,8 @@\n # Copyright(c) 2023 University of New Hampshire\n \n import json\n-from typing import TypedDict\n+from ipaddress import IPv4Interface, IPv6Interface\n+from typing import TypedDict, Union\n \n from typing_extensions import NotRequired\n \n@@ -181,3 +182,20 @@ def configure_port_state(self, port: Port, enable: bool) -> None:\n self.send_command(\n f\"ip link set dev {port.logical_name} {state}\", privileged=True\n )\n+\n+ def configure_port_ip_address(\n+ self,\n+ address: Union[IPv4Interface, IPv6Interface],\n+ port: Port,\n+ delete: bool,\n+ ) -> None:\n+ command = \"del\" if delete else \"add\"\n+ self.send_command(\n+ f\"ip address {command} {address} dev {port.logical_name}\",\n+ privileged=True,\n+ verify=True,\n+ )\n+\n+ def configure_ipv4_forwarding(self, enable: bool) -> None:\n+ state = 1 if enable else 0\n+ self.send_command(f\"sysctl -w net.ipv4.ip_forward={state}\", privileged=True)\ndiff --git a/dts/framework/remote_session/os_session.py b/dts/framework/remote_session/os_session.py\nindex ab4bfbfe4c..8a709eac1c 100644\n--- a/dts/framework/remote_session/os_session.py\n+++ b/dts/framework/remote_session/os_session.py\n@@ -4,8 +4,9 @@\n \n from abc import ABC, abstractmethod\n from collections.abc import Iterable\n+from ipaddress import IPv4Interface, IPv6Interface\n from pathlib import PurePath\n-from typing import Type, TypeVar\n+from typing import Type, TypeVar, Union\n \n from framework.config import Architecture, NodeConfiguration, NodeInfo\n from framework.logger import DTSLOG\n@@ -264,3 +265,20 @@ def configure_port_state(self, port: Port, enable: bool) -> None:\n \"\"\"\n Enable/disable port.\n \"\"\"\n+\n+ @abstractmethod\n+ def configure_port_ip_address(\n+ self,\n+ address: Union[IPv4Interface, IPv6Interface],\n+ port: Port,\n+ delete: bool,\n+ ) -> None:\n+ \"\"\"\n+ Configure (add or delete) an IP address of the input port.\n+ \"\"\"\n+\n+ @abstractmethod\n+ def configure_ipv4_forwarding(self, enable: bool) -> None:\n+ \"\"\"\n+ Enable IPv4 forwarding in the underlying OS.\n+ \"\"\"\ndiff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py\nindex 056460dd05..3b890c0451 100644\n--- a/dts/framework/test_suite.py\n+++ b/dts/framework/test_suite.py\n@@ -9,7 +9,13 @@\n import importlib\n import inspect\n import re\n+from ipaddress import IPv4Interface, IPv6Interface, ip_interface\n from types import MethodType\n+from typing import Union\n+\n+from scapy.layers.inet import IP # type: ignore[import]\n+from scapy.layers.l2 import Ether # type: ignore[import]\n+from scapy.packet import Packet, Padding # type: ignore[import]\n \n from .exception import (\n BlockingTestSuiteError,\n@@ -21,6 +27,8 @@\n from .settings import SETTINGS\n from .test_result import BuildTargetResult, Result, TestCaseResult, TestSuiteResult\n from .testbed_model import SutNode, TGNode\n+from .testbed_model.hw.port import Port, PortLink\n+from .utils import get_packet_summaries\n \n \n class TestSuite(object):\n@@ -47,6 +55,15 @@ class TestSuite(object):\n _test_cases_to_run: list[str]\n _func: bool\n _result: TestSuiteResult\n+ _port_links: list[PortLink]\n+ _sut_port_ingress: Port\n+ _sut_port_egress: Port\n+ _sut_ip_address_ingress: Union[IPv4Interface, IPv6Interface]\n+ _sut_ip_address_egress: Union[IPv4Interface, IPv6Interface]\n+ _tg_port_ingress: Port\n+ _tg_port_egress: Port\n+ _tg_ip_address_ingress: Union[IPv4Interface, IPv6Interface]\n+ _tg_ip_address_egress: Union[IPv4Interface, IPv6Interface]\n \n def __init__(\n self,\n@@ -63,6 +80,31 @@ def __init__(\n self._test_cases_to_run.extend(SETTINGS.test_cases)\n self._func = func\n self._result = build_target_result.add_test_suite(self.__class__.__name__)\n+ self._port_links = []\n+ self._process_links()\n+ self._sut_port_ingress, self._tg_port_egress = (\n+ self._port_links[0].sut_port,\n+ self._port_links[0].tg_port,\n+ )\n+ self._sut_port_egress, self._tg_port_ingress = (\n+ self._port_links[1].sut_port,\n+ self._port_links[1].tg_port,\n+ )\n+ self._sut_ip_address_ingress = ip_interface(\"192.168.100.2/24\")\n+ self._sut_ip_address_egress = ip_interface(\"192.168.101.2/24\")\n+ self._tg_ip_address_egress = ip_interface(\"192.168.100.3/24\")\n+ self._tg_ip_address_ingress = ip_interface(\"192.168.101.3/24\")\n+\n+ def _process_links(self) -> None:\n+ for sut_port in self.sut_node.ports:\n+ for tg_port in self.tg_node.ports:\n+ if (sut_port.identifier, sut_port.peer) == (\n+ tg_port.peer,\n+ tg_port.identifier,\n+ ):\n+ self._port_links.append(\n+ PortLink(sut_port=sut_port, tg_port=tg_port)\n+ )\n \n def set_up_suite(self) -> None:\n \"\"\"\n@@ -85,14 +127,181 @@ def tear_down_test_case(self) -> None:\n Tear down the previously created test fixtures after each test case.\n \"\"\"\n \n+ def configure_testbed_ipv4(self, restore: bool = False) -> None:\n+ delete = True if restore else False\n+ enable = False if restore else True\n+ self._configure_ipv4_forwarding(enable)\n+ self.sut_node.configure_port_ip_address(\n+ self._sut_ip_address_egress, self._sut_port_egress, delete\n+ )\n+ self.sut_node.configure_port_state(self._sut_port_egress, enable)\n+ self.sut_node.configure_port_ip_address(\n+ self._sut_ip_address_ingress, self._sut_port_ingress, delete\n+ )\n+ self.sut_node.configure_port_state(self._sut_port_ingress, enable)\n+ self.tg_node.configure_port_ip_address(\n+ self._tg_ip_address_ingress, self._tg_port_ingress, delete\n+ )\n+ self.tg_node.configure_port_state(self._tg_port_ingress, enable)\n+ self.tg_node.configure_port_ip_address(\n+ self._tg_ip_address_egress, self._tg_port_egress, delete\n+ )\n+ self.tg_node.configure_port_state(self._tg_port_egress, enable)\n+\n+ def _configure_ipv4_forwarding(self, enable: bool) -> None:\n+ self.sut_node.configure_ipv4_forwarding(enable)\n+\n+ def send_packet_and_capture(\n+ self, packet: Packet, duration: float = 1\n+ ) -> list[Packet]:\n+ \"\"\"\n+ Send a packet through the appropriate interface and\n+ receive on the appropriate interface.\n+ Modify the packet with l3/l2 addresses corresponding\n+ to the testbed and desired traffic.\n+ \"\"\"\n+ packet = self._adjust_addresses(packet)\n+ return self.tg_node.send_packet_and_capture(\n+ packet, self._tg_port_egress, self._tg_port_ingress, duration\n+ )\n+\n+ def get_expected_packet(self, packet: Packet) -> Packet:\n+ return self._adjust_addresses(packet, expected=True)\n+\n+ def _adjust_addresses(self, packet: Packet, expected: bool = False) -> Packet:\n+ \"\"\"\n+ Assumptions:\n+ Two links between SUT and TG, one link is TG -> SUT,\n+ the other SUT -> TG.\n+ \"\"\"\n+ if expected:\n+ # The packet enters the TG from SUT\n+ # update l2 addresses\n+ packet.src = self._sut_port_egress.mac_address\n+ packet.dst = self._tg_port_ingress.mac_address\n+\n+ # The packet is routed from TG egress to TG ingress\n+ # update l3 addresses\n+ packet.payload.src = self._tg_ip_address_egress.ip.exploded\n+ packet.payload.dst = self._tg_ip_address_ingress.ip.exploded\n+ else:\n+ # The packet leaves TG towards SUT\n+ # update l2 addresses\n+ packet.src = self._tg_port_egress.mac_address\n+ packet.dst = self._sut_port_ingress.mac_address\n+\n+ # The packet is routed from TG egress to TG ingress\n+ # update l3 addresses\n+ packet.payload.src = self._tg_ip_address_egress.ip.exploded\n+ packet.payload.dst = self._tg_ip_address_ingress.ip.exploded\n+\n+ return Ether(packet.build())\n+\n def verify(self, condition: bool, failure_description: str) -> None:\n if not condition:\n+ self._fail_test_case_verify(failure_description)\n+\n+ def _fail_test_case_verify(self, failure_description: str) -> None:\n+ self._logger.debug(\n+ \"A test case failed, showing the last 10 commands executed on SUT:\"\n+ )\n+ for command_res in self.sut_node.main_session.remote_session.history[-10:]:\n+ self._logger.debug(command_res.command)\n+ self._logger.debug(\n+ \"A test case failed, showing the last 10 commands executed on TG:\"\n+ )\n+ for command_res in self.tg_node.main_session.remote_session.history[-10:]:\n+ self._logger.debug(command_res.command)\n+ raise TestCaseVerifyError(failure_description)\n+\n+ def verify_packets(\n+ self, expected_packet: Packet, received_packets: list[Packet]\n+ ) -> None:\n+ for received_packet in received_packets:\n+ if self._compare_packets(expected_packet, received_packet):\n+ break\n+ else:\n+ self._logger.debug(\n+ f\"The expected packet {get_packet_summaries(expected_packet)} \"\n+ f\"not found among received {get_packet_summaries(received_packets)}\"\n+ )\n+ self._fail_test_case_verify(\n+ \"An expected packet not found among received packets.\"\n+ )\n+\n+ def _compare_packets(\n+ self, expected_packet: Packet, received_packet: Packet\n+ ) -> bool:\n+ self._logger.debug(\n+ \"Comparing packets: \\n\"\n+ f\"{expected_packet.summary()}\\n\"\n+ f\"{received_packet.summary()}\"\n+ )\n+\n+ l3 = IP in expected_packet.layers()\n+ self._logger.debug(\"Found l3 layer\")\n+\n+ received_payload = received_packet\n+ expected_payload = expected_packet\n+ while received_payload and expected_payload:\n+ self._logger.debug(\"Comparing payloads:\")\n+ self._logger.debug(f\"Received: {received_payload}\")\n+ self._logger.debug(f\"Expected: {expected_payload}\")\n+ if received_payload.__class__ == expected_payload.__class__:\n+ self._logger.debug(\"The layers are the same.\")\n+ if received_payload.__class__ == Ether:\n+ if not self._verify_l2_frame(received_payload, l3):\n+ return False\n+ elif received_payload.__class__ == IP:\n+ if not self._verify_l3_packet(received_payload, expected_payload):\n+ return False\n+ else:\n+ # Different layers => different packets\n+ return False\n+ received_payload = received_payload.payload\n+ expected_payload = expected_payload.payload\n+\n+ if expected_payload:\n self._logger.debug(\n- \"A test case failed, showing the last 10 commands executed on SUT:\"\n+ f\"The expected packet did not contain {expected_payload}.\"\n )\n- for command_res in self.sut_node.main_session.remote_session.history[-10:]:\n- self._logger.debug(command_res.command)\n- raise TestCaseVerifyError(failure_description)\n+ return False\n+ if received_payload and received_payload.__class__ != Padding:\n+ self._logger.debug(\n+ \"The received payload had extra layers which were not padding.\"\n+ )\n+ return False\n+ return True\n+\n+ def _verify_l2_frame(self, received_packet: Ether, l3: bool) -> bool:\n+ self._logger.debug(\"Looking at the Ether layer.\")\n+ self._logger.debug(\n+ f\"Comparing received dst mac '{received_packet.dst}' \"\n+ f\"with expected '{self._tg_port_ingress.mac_address}'.\"\n+ )\n+ if received_packet.dst != self._tg_port_ingress.mac_address:\n+ return False\n+\n+ expected_src_mac = self._tg_port_egress.mac_address\n+ if l3:\n+ expected_src_mac = self._sut_port_egress.mac_address\n+ self._logger.debug(\n+ f\"Comparing received src mac '{received_packet.src}' \"\n+ f\"with expected '{expected_src_mac}'.\"\n+ )\n+ if received_packet.src != expected_src_mac:\n+ return False\n+\n+ return True\n+\n+ def _verify_l3_packet(self, received_packet: IP, expected_packet: IP) -> bool:\n+ self._logger.debug(\"Looking at the IP layer.\")\n+ if (\n+ received_packet.src != expected_packet.src\n+ or received_packet.dst != expected_packet.dst\n+ ):\n+ return False\n+ return True\n \n def run(self) -> None:\n \"\"\"\ndiff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py\nindex c666dfbf4e..fc01e0bf8e 100644\n--- a/dts/framework/testbed_model/node.py\n+++ b/dts/framework/testbed_model/node.py\n@@ -8,7 +8,8 @@\n \"\"\"\n \n from abc import ABC\n-from typing import Any, Callable, Type\n+from ipaddress import IPv4Interface, IPv6Interface\n+from typing import Any, Callable, Type, Union\n \n from framework.config import (\n BuildTargetConfiguration,\n@@ -221,6 +222,17 @@ def configure_port_state(self, port: Port, enable: bool = True) -> None:\n \"\"\"\n self.main_session.configure_port_state(port, enable)\n \n+ def configure_port_ip_address(\n+ self,\n+ address: Union[IPv4Interface, IPv6Interface],\n+ port: Port,\n+ delete: bool = False,\n+ ) -> None:\n+ \"\"\"\n+ Configure the IP address of a port on this node.\n+ \"\"\"\n+ self.main_session.configure_port_ip_address(address, port, delete)\n+\n def close(self) -> None:\n \"\"\"\n Close all connections and free other resources.\ndiff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbed_model/sut_node.py\nindex f0b017a383..202aebfd06 100644\n--- a/dts/framework/testbed_model/sut_node.py\n+++ b/dts/framework/testbed_model/sut_node.py\n@@ -351,6 +351,9 @@ def run_dpdk_app(\n f\"{app_path} {eal_args}\", timeout, privileged=True, verify=True\n )\n \n+ def configure_ipv4_forwarding(self, enable: bool) -> None:\n+ self.main_session.configure_ipv4_forwarding(enable)\n+\n def create_interactive_shell(\n self,\n shell_cls: Type[InteractiveShellType],\ndiff --git a/dts/tests/TestSuite_os_udp.py b/dts/tests/TestSuite_os_udp.py\nnew file mode 100644\nindex 0000000000..9b5f39711d\n--- /dev/null\n+++ b/dts/tests/TestSuite_os_udp.py\n@@ -0,0 +1,45 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2023 PANTHEON.tech s.r.o.\n+\n+\"\"\"\n+Configure SUT node to route traffic from if1 to if2.\n+Send a packet to the SUT node, verify it comes back on the second port on the TG node.\n+\"\"\"\n+\n+from scapy.layers.inet import IP, UDP # type: ignore[import]\n+from scapy.layers.l2 import Ether # type: ignore[import]\n+\n+from framework.test_suite import TestSuite\n+\n+\n+class TestOSUdp(TestSuite):\n+ def set_up_suite(self) -> None:\n+ \"\"\"\n+ Setup:\n+ Configure SUT ports and SUT to route traffic from if1 to if2.\n+ \"\"\"\n+\n+ self.configure_testbed_ipv4()\n+\n+ def test_os_udp(self) -> None:\n+ \"\"\"\n+ Steps:\n+ Send a UDP packet.\n+ Verify:\n+ The packet with proper addresses arrives at the other TG port.\n+ \"\"\"\n+\n+ packet = Ether() / IP() / UDP()\n+\n+ received_packets = self.send_packet_and_capture(packet)\n+\n+ expected_packet = self.get_expected_packet(packet)\n+\n+ self.verify_packets(expected_packet, received_packets)\n+\n+ def tear_down_suite(self) -> None:\n+ \"\"\"\n+ Teardown:\n+ Remove the SUT port configuration configured in setup.\n+ \"\"\"\n+ self.configure_testbed_ipv4(restore=True)\n", "prefixes": [ "v3", "6/6" ] }{ "id": 129646, "url": "