Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/124465/?format=api
http://patches.dpdk.org/api/patches/124465/?format=api", "web_url": "http://patches.dpdk.org/project/dpdk/patch/20230223152840.634183-5-juraj.linkes@pantheon.tech/", "project": { "id": 1, "url": "http://patches.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": "<20230223152840.634183-5-juraj.linkes@pantheon.tech>", "list_archive_url": "https://inbox.dpdk.org/dev/20230223152840.634183-5-juraj.linkes@pantheon.tech", "date": "2023-02-23T15:28:34", "name": "[v5,04/10] dts: add dpdk execution handling", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": true, "hash": "e55b8996c6cb022da24381d858f14baa9853e8d8", "submitter": { "id": 1626, "url": "http://patches.dpdk.org/api/people/1626/?format=api", "name": "Juraj Linkeš", "email": "juraj.linkes@pantheon.tech" }, "delegate": { "id": 1, "url": "http://patches.dpdk.org/api/users/1/?format=api", "username": "tmonjalo", "first_name": "Thomas", "last_name": "Monjalon", "email": "thomas@monjalon.net" }, "mbox": "http://patches.dpdk.org/project/dpdk/patch/20230223152840.634183-5-juraj.linkes@pantheon.tech/mbox/", "series": [ { "id": 27159, "url": "http://patches.dpdk.org/api/series/27159/?format=api", "web_url": "http://patches.dpdk.org/project/dpdk/list/?series=27159", "date": "2023-02-23T15:28:30", "name": "dts: add hello world testcase", "version": 5, "mbox": "http://patches.dpdk.org/series/27159/mbox/" } ], "comments": "http://patches.dpdk.org/api/patches/124465/comments/", "check": "success", "checks": "http://patches.dpdk.org/api/patches/124465/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 A5E3541D53;\n\tThu, 23 Feb 2023 16:29:26 +0100 (CET)", "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id F070542D67;\n\tThu, 23 Feb 2023 16:28:57 +0100 (CET)", "from mail-ed1-f53.google.com (mail-ed1-f53.google.com\n [209.85.208.53]) by mails.dpdk.org (Postfix) with ESMTP id 9FB0040ED5\n for <dev@dpdk.org>; Thu, 23 Feb 2023 16:28:49 +0100 (CET)", "by mail-ed1-f53.google.com with SMTP id cq23so43171452edb.1\n for <dev@dpdk.org>; Thu, 23 Feb 2023 07:28:49 -0800 (PST)", "from localhost.localdomain ([84.245.121.112])\n by smtp.gmail.com with ESMTPSA id\n r6-20020a50c006000000b004af6a8617ffsm1158892edb.46.2023.02.23.07.28.48\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Thu, 23 Feb 2023 07:28:49 -0800 (PST)" ], "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=pantheon-tech.20210112.gappssmtp.com; s=20210112;\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=1ZWyigxmbQMZ38U5+4Kee8ifdGr8PILyWiu3+Cu7GWU=;\n b=8PeaIl+LQLYZd5UtVP4ZDGBDDDWeH7eL4OUmg9U5R2iayrts8arNq8kXFASGUux5EV\n vDm59UJnlp0J5UZcHJK/rQuaQs9aNuBUyKPUXJcssL+X8jBEnHx4Z4OIG60jm+zZPISl\n NLDUPENAWo2SMU/Ygi0V6Q3HY5JHOP/Kb5z4RGk15rXDZJN2/ZiaV3fUwrhq76QnYfcJ\n Tv7BzR/2oqIxZKMWl3sKpzpI2YcdShy8fUN9SNgEDOJ5rbYNWZ05X6dyfVN5gGaYXIQ5\n DP4SMcD747OK+ArJlKdQByiussWGpoduIMfUy6JiHkNdXYHAOMUPP3YscNqfZ0xVvAtb\n GRpA==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20210112;\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=1ZWyigxmbQMZ38U5+4Kee8ifdGr8PILyWiu3+Cu7GWU=;\n b=FdXV8oN+aIpnKeJKtr39kJ6p5Xcefh7Pr2PjHb1HX0VX/xe00cgY4drlA0r/2NHZua\n yhqbyvXW5ghpxaN9WTITF7ynTnfUdQVBUCjhJIF4ucLAf4pGcGOV84iU4D4UcJbDiyH5\n NAUP3WCrIJG4xWVMYtpYxPfR+hUbOkkG/kqNNI3SiLFIdbJIPYPBANdgJdNydfoqQ6/X\n Q6lFqUQWELJ0UZRn3ok3qKdZjWY0lR5BjUwWnQBPdipSup1NdjmgHRpYG4dIJzqouovW\n jK0jSorLAyaJjpdset8J9Q3LXrqKRyXGSq32b5PcFcKoKhJcaaAaXGx7n+rfZ006P07b\n 2A6w==", "X-Gm-Message-State": "AO0yUKWjIxmVAoelYvmMe9HbpsJ4ET5JpfhxISjzxUBiv+6wpoWHAUAx\n eypX3+hC1OR9+XPfQWmsYLfOPQ==", "X-Google-Smtp-Source": "\n AK7set+AIwDyR3nHK9+lm5T7YMHuwQjeEHzETp5l8btFHkRwKb+yIT9r1VWUQ6RCQqSV4WLzWNGrhw==", "X-Received": "by 2002:aa7:d6d4:0:b0:4af:601c:ea93 with SMTP id\n x20-20020aa7d6d4000000b004af601cea93mr6316365edr.10.1677166129290;\n Thu, 23 Feb 2023 07:28:49 -0800 (PST)", "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, probb@iol.unh.edu", "Cc": "dev@dpdk.org, =?utf-8?q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>", "Subject": "[PATCH v5 04/10] dts: add dpdk execution handling", "Date": "Thu, 23 Feb 2023 16:28:34 +0100", "Message-Id": "<20230223152840.634183-5-juraj.linkes@pantheon.tech>", "X-Mailer": "git-send-email 2.30.2", "In-Reply-To": "<20230223152840.634183-1-juraj.linkes@pantheon.tech>", "References": "<20230213152846.284191-1-juraj.linkes@pantheon.tech>\n <20230223152840.634183-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": "Add methods for setting up and shutting down DPDK apps and for\nconstructing EAL parameters.\n\nSigned-off-by: Juraj Linkeš <juraj.linkes@pantheon.tech>\n---\n dts/conf.yaml | 4 +\n dts/framework/config/__init__.py | 8 +\n dts/framework/config/conf_yaml_schema.json | 25 ++\n dts/framework/remote_session/linux_session.py | 18 ++\n dts/framework/remote_session/os_session.py | 23 +-\n dts/framework/remote_session/posix_session.py | 83 ++++++\n dts/framework/testbed_model/__init__.py | 8 +\n dts/framework/testbed_model/dpdk.py | 45 +++\n dts/framework/testbed_model/hw/__init__.py | 27 ++\n dts/framework/testbed_model/hw/cpu.py | 274 ++++++++++++++++++\n .../testbed_model/hw/virtual_device.py | 16 +\n dts/framework/testbed_model/node.py | 43 +++\n dts/framework/testbed_model/sut_node.py | 81 +++++-\n dts/framework/utils.py | 20 ++\n 14 files changed, 673 insertions(+), 2 deletions(-)\n create mode 100644 dts/framework/testbed_model/hw/__init__.py\n create mode 100644 dts/framework/testbed_model/hw/cpu.py\n create mode 100644 dts/framework/testbed_model/hw/virtual_device.py", "diff": "diff --git a/dts/conf.yaml b/dts/conf.yaml\nindex 03696d2bab..1648e5c3c5 100644\n--- a/dts/conf.yaml\n+++ b/dts/conf.yaml\n@@ -13,4 +13,8 @@ nodes:\n - name: \"SUT 1\"\n hostname: sut1.change.me.localhost\n user: root\n+ arch: x86_64\n os: linux\n+ lcores: \"\"\n+ use_first_core: false\n+ memory_channels: 4\ndiff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py\nindex ca61cb10fe..17b917f3b3 100644\n--- a/dts/framework/config/__init__.py\n+++ b/dts/framework/config/__init__.py\n@@ -72,7 +72,11 @@ class NodeConfiguration:\n hostname: str\n user: str\n password: str | None\n+ arch: Architecture\n os: OS\n+ lcores: str\n+ use_first_core: bool\n+ memory_channels: int\n \n @staticmethod\n def from_dict(d: dict) -> \"NodeConfiguration\":\n@@ -81,7 +85,11 @@ def from_dict(d: dict) -> \"NodeConfiguration\":\n hostname=d[\"hostname\"],\n user=d[\"user\"],\n password=d.get(\"password\"),\n+ arch=Architecture(d[\"arch\"]),\n os=OS(d[\"os\"]),\n+ lcores=d.get(\"lcores\", \"1\"),\n+ use_first_core=d.get(\"use_first_core\", False),\n+ memory_channels=d.get(\"memory_channels\", 1),\n )\n \n \ndiff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/config/conf_yaml_schema.json\nindex 9170307fbe..334b4bd8ab 100644\n--- a/dts/framework/config/conf_yaml_schema.json\n+++ b/dts/framework/config/conf_yaml_schema.json\n@@ -6,6 +6,14 @@\n \"type\": \"string\",\n \"description\": \"A unique identifier for a node\"\n },\n+ \"ARCH\": {\n+ \"type\": \"string\",\n+ \"enum\": [\n+ \"x86_64\",\n+ \"arm64\",\n+ \"ppc64le\"\n+ ]\n+ },\n \"OS\": {\n \"type\": \"string\",\n \"enum\": [\n@@ -92,8 +100,24 @@\n \"type\": \"string\",\n \"description\": \"The password to use on this node. Use only as a last resort. SSH keys are STRONGLY preferred.\"\n },\n+ \"arch\": {\n+ \"$ref\": \"#/definitions/ARCH\"\n+ },\n \"os\": {\n \"$ref\": \"#/definitions/OS\"\n+ },\n+ \"lcores\": {\n+ \"type\": \"string\",\n+ \"pattern\": \"^(([0-9]+|([0-9]+-[0-9]+))(,([0-9]+|([0-9]+-[0-9]+)))*)?$\",\n+ \"description\": \"Optional comma-separated list of logical cores to use, e.g.: 1,2,3,4,5,18-22. Defaults to 1. An empty string means use all lcores.\"\n+ },\n+ \"use_first_core\": {\n+ \"type\": \"boolean\",\n+ \"description\": \"Indicate whether DPDK should use the first physical core. It won't be used by default.\"\n+ },\n+ \"memory_channels\": {\n+ \"type\": \"integer\",\n+ \"description\": \"How many memory channels to use. Optional, defaults to 1.\"\n }\n },\n \"additionalProperties\": false,\n@@ -101,6 +125,7 @@\n \"name\",\n \"hostname\",\n \"user\",\n+ \"arch\",\n \"os\"\n ]\n },\ndiff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/remote_session/linux_session.py\nindex 9d14166077..c49b6bb1d7 100644\n--- a/dts/framework/remote_session/linux_session.py\n+++ b/dts/framework/remote_session/linux_session.py\n@@ -2,6 +2,8 @@\n # Copyright(c) 2023 PANTHEON.tech s.r.o.\n # Copyright(c) 2023 University of New Hampshire\n \n+from framework.testbed_model import LogicalCore\n+\n from .posix_session import PosixSession\n \n \n@@ -9,3 +11,19 @@ class LinuxSession(PosixSession):\n \"\"\"\n The implementation of non-Posix compliant parts of Linux remote sessions.\n \"\"\"\n+\n+ def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:\n+ cpu_info = self.remote_session.send_command(\n+ \"lscpu -p=CPU,CORE,SOCKET,NODE|grep -v \\\\#\"\n+ ).stdout\n+ lcores = []\n+ for cpu_line in cpu_info.splitlines():\n+ lcore, core, socket, node = map(int, cpu_line.split(\",\"))\n+ if core == 0 and socket == 0 and not use_first_core:\n+ self._logger.info(\"Not using the first physical core.\")\n+ continue\n+ lcores.append(LogicalCore(lcore, core, socket, node))\n+ return lcores\n+\n+ def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+ return dpdk_prefix\ndiff --git a/dts/framework/remote_session/os_session.py b/dts/framework/remote_session/os_session.py\nindex 06d1ffefdd..c30753e0b8 100644\n--- a/dts/framework/remote_session/os_session.py\n+++ b/dts/framework/remote_session/os_session.py\n@@ -3,12 +3,13 @@\n # Copyright(c) 2023 University of New Hampshire\n \n from abc import ABC, abstractmethod\n+from collections.abc import Iterable\n from pathlib import PurePath\n \n from framework.config import Architecture, NodeConfiguration\n from framework.logger import DTSLOG\n from framework.settings import SETTINGS\n-from framework.testbed_model import MesonArgs\n+from framework.testbed_model import LogicalCore, MesonArgs\n from framework.utils import EnvVarsDict\n \n from .remote import RemoteSession, create_remote_session\n@@ -130,3 +131,23 @@ def get_dpdk_version(self, version_path: str | PurePath) -> str:\n \"\"\"\n Inspect DPDK version on the remote node from version_path.\n \"\"\"\n+\n+ @abstractmethod\n+ def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:\n+ \"\"\"\n+ Compose a list of LogicalCores present on the remote node.\n+ If use_first_core is False, the first physical core won't be used.\n+ \"\"\"\n+\n+ @abstractmethod\n+ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:\n+ \"\"\"\n+ Kill and cleanup all DPDK apps identified by dpdk_prefix_list. If\n+ dpdk_prefix_list is empty, attempt to find running DPDK apps to kill and clean.\n+ \"\"\"\n+\n+ @abstractmethod\n+ def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+ \"\"\"\n+ Get the DPDK file prefix that will be used when running DPDK apps.\n+ \"\"\"\ndiff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/remote_session/posix_session.py\nindex 7a5c38c36e..9b05464d65 100644\n--- a/dts/framework/remote_session/posix_session.py\n+++ b/dts/framework/remote_session/posix_session.py\n@@ -2,6 +2,8 @@\n # Copyright(c) 2023 PANTHEON.tech s.r.o.\n # Copyright(c) 2023 University of New Hampshire\n \n+import re\n+from collections.abc import Iterable\n from pathlib import PurePath, PurePosixPath\n \n from framework.config import Architecture\n@@ -137,3 +139,84 @@ def get_dpdk_version(self, build_dir: str | PurePath) -> str:\n f\"cat {self.join_remote_path(build_dir, 'VERSION')}\", verify=True\n )\n return out.stdout\n+\n+ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:\n+ self._logger.info(\"Cleaning up DPDK apps.\")\n+ dpdk_runtime_dirs = self._get_dpdk_runtime_dirs(dpdk_prefix_list)\n+ if dpdk_runtime_dirs:\n+ # kill and cleanup only if DPDK is running\n+ dpdk_pids = self._get_dpdk_pids(dpdk_runtime_dirs)\n+ for dpdk_pid in dpdk_pids:\n+ self.remote_session.send_command(f\"kill -9 {dpdk_pid}\", 20)\n+ self._check_dpdk_hugepages(dpdk_runtime_dirs)\n+ self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs)\n+\n+ def _get_dpdk_runtime_dirs(\n+ self, dpdk_prefix_list: Iterable[str]\n+ ) -> list[PurePosixPath]:\n+ prefix = PurePosixPath(\"/var\", \"run\", \"dpdk\")\n+ if not dpdk_prefix_list:\n+ remote_prefixes = self._list_remote_dirs(prefix)\n+ if not remote_prefixes:\n+ dpdk_prefix_list = []\n+ else:\n+ dpdk_prefix_list = remote_prefixes\n+\n+ return [PurePosixPath(prefix, dpdk_prefix) for dpdk_prefix in dpdk_prefix_list]\n+\n+ def _list_remote_dirs(self, remote_path: str | PurePath) -> list[str] | None:\n+ \"\"\"\n+ Return a list of directories of the remote_dir.\n+ If remote_path doesn't exist, return None.\n+ \"\"\"\n+ out = self.remote_session.send_command(\n+ f\"ls -l {remote_path} | awk '/^d/ {{print $NF}}'\"\n+ ).stdout\n+ if \"No such file or directory\" in out:\n+ return None\n+ else:\n+ return out.splitlines()\n+\n+ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> list[int]:\n+ pids = []\n+ pid_regex = r\"p(\\d+)\"\n+ for dpdk_runtime_dir in dpdk_runtime_dirs:\n+ dpdk_config_file = PurePosixPath(dpdk_runtime_dir, \"config\")\n+ if self._remote_files_exists(dpdk_config_file):\n+ out = self.remote_session.send_command(\n+ f\"lsof -Fp {dpdk_config_file}\"\n+ ).stdout\n+ if out and \"No such file or directory\" not in out:\n+ for out_line in out.splitlines():\n+ match = re.match(pid_regex, out_line)\n+ if match:\n+ pids.append(int(match.group(1)))\n+ return pids\n+\n+ def _remote_files_exists(self, remote_path: PurePath) -> bool:\n+ result = self.remote_session.send_command(f\"test -e {remote_path}\")\n+ return not result.return_code\n+\n+ def _check_dpdk_hugepages(\n+ self, dpdk_runtime_dirs: Iterable[str | PurePath]\n+ ) -> None:\n+ for dpdk_runtime_dir in dpdk_runtime_dirs:\n+ hugepage_info = PurePosixPath(dpdk_runtime_dir, \"hugepage_info\")\n+ if self._remote_files_exists(hugepage_info):\n+ out = self.remote_session.send_command(\n+ f\"lsof -Fp {hugepage_info}\"\n+ ).stdout\n+ if out and \"No such file or directory\" not in out:\n+ self._logger.warning(\"Some DPDK processes did not free hugepages.\")\n+ self._logger.warning(\"*******************************************\")\n+ self._logger.warning(out)\n+ self._logger.warning(\"*******************************************\")\n+\n+ def _remove_dpdk_runtime_dirs(\n+ self, dpdk_runtime_dirs: Iterable[str | PurePath]\n+ ) -> None:\n+ for dpdk_runtime_dir in dpdk_runtime_dirs:\n+ self.remove_remote_dir(dpdk_runtime_dir)\n+\n+ def get_dpdk_file_prefix(self, dpdk_prefix) -> str:\n+ return \"\"\ndiff --git a/dts/framework/testbed_model/__init__.py b/dts/framework/testbed_model/__init__.py\nindex 96e2ab7c3f..22c7c16708 100644\n--- a/dts/framework/testbed_model/__init__.py\n+++ b/dts/framework/testbed_model/__init__.py\n@@ -10,5 +10,13 @@\n # pylama:ignore=W0611\n \n from .dpdk import MesonArgs\n+from .hw import (\n+ LogicalCore,\n+ LogicalCoreCount,\n+ LogicalCoreList,\n+ LogicalCoreListFilter,\n+ VirtualDevice,\n+ lcore_filter,\n+)\n from .node import Node\n from .sut_node import SutNode\ndiff --git a/dts/framework/testbed_model/dpdk.py b/dts/framework/testbed_model/dpdk.py\nindex 0526974f72..9b3a9e7381 100644\n--- a/dts/framework/testbed_model/dpdk.py\n+++ b/dts/framework/testbed_model/dpdk.py\n@@ -6,6 +6,8 @@\n Various utilities used for configuring, building and running DPDK.\n \"\"\"\n \n+from .hw import LogicalCoreList, VirtualDevice\n+\n \n class MesonArgs(object):\n \"\"\"\n@@ -31,3 +33,46 @@ def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):\n \n def __str__(self) -> str:\n return \" \".join(f\"{self.default_library} {self.dpdk_args}\".split())\n+\n+\n+class EalParameters(object):\n+ def __init__(\n+ self,\n+ lcore_list: LogicalCoreList,\n+ memory_channels: int,\n+ prefix: str,\n+ no_pci: bool,\n+ vdevs: list[VirtualDevice],\n+ other_eal_param: str,\n+ ):\n+ \"\"\"\n+ Generate eal parameters character string;\n+ :param lcore_list: the list of logical cores to use.\n+ :param memory_channels: the number of memory channels to use.\n+ :param prefix: set file prefix string, eg:\n+ prefix='vf'\n+ :param no_pci: switch of disable PCI bus eg:\n+ no_pci=True\n+ :param vdevs: virtual device list, eg:\n+ vdevs=['net_ring0', 'net_ring1']\n+ :param other_eal_param: user defined DPDK eal parameters, eg:\n+ other_eal_param='--single-file-segments'\n+ \"\"\"\n+ self._lcore_list = f\"-l {lcore_list}\"\n+ self._memory_channels = f\"-n {memory_channels}\"\n+ self._prefix = prefix\n+ if prefix:\n+ self._prefix = f\"--file-prefix={prefix}\"\n+ self._no_pci = \"--no-pci\" if no_pci else \"\"\n+ self._vdevs = \" \".join(f\"--vdev {vdev}\" for vdev in vdevs)\n+ self._other_eal_param = other_eal_param\n+\n+ def __str__(self) -> str:\n+ return (\n+ f\"{self._lcore_list} \"\n+ f\"{self._memory_channels} \"\n+ f\"{self._prefix} \"\n+ f\"{self._no_pci} \"\n+ f\"{self._vdevs} \"\n+ f\"{self._other_eal_param}\"\n+ )\ndiff --git a/dts/framework/testbed_model/hw/__init__.py b/dts/framework/testbed_model/hw/__init__.py\nnew file mode 100644\nindex 0000000000..88ccac0b0e\n--- /dev/null\n+++ b/dts/framework/testbed_model/hw/__init__.py\n@@ -0,0 +1,27 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2023 PANTHEON.tech s.r.o.\n+\n+# pylama:ignore=W0611\n+\n+from .cpu import (\n+ LogicalCore,\n+ LogicalCoreCount,\n+ LogicalCoreCountFilter,\n+ LogicalCoreFilter,\n+ LogicalCoreList,\n+ LogicalCoreListFilter,\n+)\n+from .virtual_device import VirtualDevice\n+\n+\n+def lcore_filter(\n+ core_list: list[LogicalCore],\n+ filter_specifier: LogicalCoreCount | LogicalCoreList,\n+ ascending: bool,\n+) -> LogicalCoreFilter:\n+ if isinstance(filter_specifier, LogicalCoreList):\n+ return LogicalCoreListFilter(core_list, filter_specifier, ascending)\n+ elif isinstance(filter_specifier, LogicalCoreCount):\n+ return LogicalCoreCountFilter(core_list, filter_specifier, ascending)\n+ else:\n+ raise ValueError(f\"Unsupported filter r{filter_specifier}\")\ndiff --git a/dts/framework/testbed_model/hw/cpu.py b/dts/framework/testbed_model/hw/cpu.py\nnew file mode 100644\nindex 0000000000..d1918a12dc\n--- /dev/null\n+++ b/dts/framework/testbed_model/hw/cpu.py\n@@ -0,0 +1,274 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2023 PANTHEON.tech s.r.o.\n+\n+import dataclasses\n+from abc import ABC, abstractmethod\n+from collections.abc import Iterable, ValuesView\n+from dataclasses import dataclass\n+\n+from framework.utils import expand_range\n+\n+\n+@dataclass(slots=True, frozen=True)\n+class LogicalCore(object):\n+ \"\"\"\n+ Representation of a CPU core. A physical core is represented in OS\n+ by multiple logical cores (lcores) if CPU multithreading is enabled.\n+ \"\"\"\n+\n+ lcore: int\n+ core: int\n+ socket: int\n+ node: int\n+\n+ def __int__(self) -> int:\n+ return self.lcore\n+\n+\n+class LogicalCoreList(object):\n+ \"\"\"\n+ Convert these options into a list of logical core ids.\n+ lcore_list=[LogicalCore1, LogicalCore2] - a list of LogicalCores\n+ lcore_list=[0,1,2,3] - a list of int indices\n+ lcore_list=['0','1','2-3'] - a list of str indices; ranges are supported\n+ lcore_list='0,1,2-3' - a comma delimited str of indices; ranges are supported\n+\n+ The class creates a unified format used across the framework and allows\n+ the user to use either a str representation (using str(instance) or directly\n+ in f-strings) or a list representation (by accessing instance.lcore_list).\n+ Empty lcore_list is allowed.\n+ \"\"\"\n+\n+ _lcore_list: list[int]\n+ _lcore_str: str\n+\n+ def __init__(self, lcore_list: list[int] | list[str] | list[LogicalCore] | str):\n+ self._lcore_list = []\n+ if isinstance(lcore_list, str):\n+ lcore_list = lcore_list.split(\",\")\n+ for lcore in lcore_list:\n+ if isinstance(lcore, str):\n+ self._lcore_list.extend(expand_range(lcore))\n+ else:\n+ self._lcore_list.append(int(lcore))\n+\n+ # the input lcores may not be sorted\n+ self._lcore_list.sort()\n+ self._lcore_str = (\n+ f'{\",\".join(self._get_consecutive_lcores_range(self._lcore_list))}'\n+ )\n+\n+ @property\n+ def lcore_list(self) -> list[int]:\n+ return self._lcore_list\n+\n+ def _get_consecutive_lcores_range(self, lcore_ids_list: list[int]) -> list[str]:\n+ formatted_core_list = []\n+ segment = lcore_ids_list[:1]\n+ for lcore_id in lcore_ids_list[1:]:\n+ if lcore_id - segment[-1] == 1:\n+ segment.append(lcore_id)\n+ else:\n+ formatted_core_list.append(\n+ f\"{segment[0]}-{segment[-1]}\"\n+ if len(segment) > 1\n+ else f\"{segment[0]}\"\n+ )\n+ current_core_index = lcore_ids_list.index(lcore_id)\n+ formatted_core_list.extend(\n+ self._get_consecutive_lcores_range(\n+ lcore_ids_list[current_core_index:]\n+ )\n+ )\n+ segment.clear()\n+ break\n+ if len(segment) > 0:\n+ formatted_core_list.append(\n+ f\"{segment[0]}-{segment[-1]}\" if len(segment) > 1 else f\"{segment[0]}\"\n+ )\n+ return formatted_core_list\n+\n+ def __str__(self) -> str:\n+ return self._lcore_str\n+\n+\n+@dataclasses.dataclass(slots=True, frozen=True)\n+class LogicalCoreCount(object):\n+ \"\"\"\n+ Define the number of logical cores to use.\n+ If sockets is not None, socket_count is ignored.\n+ \"\"\"\n+\n+ lcores_per_core: int = 1\n+ cores_per_socket: int = 2\n+ socket_count: int = 1\n+ sockets: list[int] | None = None\n+\n+\n+class LogicalCoreFilter(ABC):\n+ \"\"\"\n+ Filter according to the input filter specifier. Each filter needs to be\n+ implemented in a derived class.\n+ This class only implements operations common to all filters, such as sorting\n+ the list to be filtered beforehand.\n+ \"\"\"\n+\n+ _filter_specifier: LogicalCoreCount | LogicalCoreList\n+ _lcores_to_filter: list[LogicalCore]\n+\n+ def __init__(\n+ self,\n+ lcore_list: list[LogicalCore],\n+ filter_specifier: LogicalCoreCount | LogicalCoreList,\n+ ascending: bool = True,\n+ ):\n+ self._filter_specifier = filter_specifier\n+\n+ # sorting by core is needed in case hyperthreading is enabled\n+ self._lcores_to_filter = sorted(\n+ lcore_list, key=lambda x: x.core, reverse=not ascending\n+ )\n+ self.filter()\n+\n+ @abstractmethod\n+ def filter(self) -> list[LogicalCore]:\n+ \"\"\"\n+ Use self._filter_specifier to filter self._lcores_to_filter\n+ and return the list of filtered LogicalCores.\n+ self._lcores_to_filter is a sorted copy of the original list,\n+ so it may be modified.\n+ \"\"\"\n+\n+\n+class LogicalCoreCountFilter(LogicalCoreFilter):\n+ \"\"\"\n+ Filter the input list of LogicalCores according to specified rules:\n+ Use cores from the specified number of sockets or from the specified socket ids.\n+ If sockets is specified, it takes precedence over socket_count.\n+ From each of those sockets, use only cores_per_socket of cores.\n+ And for each core, use lcores_per_core of logical cores. Hypertheading\n+ must be enabled for this to take effect.\n+ If ascending is True, use cores with the lowest numerical id first\n+ and continue in ascending order. If False, start with the highest\n+ id and continue in descending order. This ordering affects which\n+ sockets to consider first as well.\n+ \"\"\"\n+\n+ _filter_specifier: LogicalCoreCount\n+\n+ def filter(self) -> list[LogicalCore]:\n+ sockets_to_filter = self._filter_sockets(self._lcores_to_filter)\n+ filtered_lcores = []\n+ for socket_to_filter in sockets_to_filter:\n+ filtered_lcores.extend(self._filter_cores_from_socket(socket_to_filter))\n+ return filtered_lcores\n+\n+ def _filter_sockets(\n+ self, lcores_to_filter: Iterable[LogicalCore]\n+ ) -> ValuesView[list[LogicalCore]]:\n+ \"\"\"\n+ Remove all lcores that don't match the specified socket(s).\n+ If self._filter_specifier.sockets is not None, keep lcores from those sockets,\n+ otherwise keep lcores from the first\n+ self._filter_specifier.socket_count sockets.\n+ \"\"\"\n+ allowed_sockets: set[int] = set()\n+ socket_count = self._filter_specifier.socket_count\n+ if self._filter_specifier.sockets:\n+ socket_count = len(self._filter_specifier.sockets)\n+ allowed_sockets = set(self._filter_specifier.sockets)\n+\n+ filtered_lcores: dict[int, list[LogicalCore]] = {}\n+ for lcore in lcores_to_filter:\n+ if not self._filter_specifier.sockets:\n+ if len(allowed_sockets) < socket_count:\n+ allowed_sockets.add(lcore.socket)\n+ if lcore.socket in allowed_sockets:\n+ if lcore.socket in filtered_lcores:\n+ filtered_lcores[lcore.socket].append(lcore)\n+ else:\n+ filtered_lcores[lcore.socket] = [lcore]\n+\n+ if len(allowed_sockets) < socket_count:\n+ raise ValueError(\n+ f\"The actual number of sockets from which to use cores \"\n+ f\"({len(allowed_sockets)}) is lower than required ({socket_count}).\"\n+ )\n+\n+ return filtered_lcores.values()\n+\n+ def _filter_cores_from_socket(\n+ self, lcores_to_filter: Iterable[LogicalCore]\n+ ) -> list[LogicalCore]:\n+ \"\"\"\n+ Keep only the first self._filter_specifier.cores_per_socket cores.\n+ In multithreaded environments, keep only\n+ the first self._filter_specifier.lcores_per_core lcores of those cores.\n+ \"\"\"\n+\n+ # no need to use ordered dict, from Python3.7 the dict\n+ # insertion order is preserved (LIFO).\n+ lcore_count_per_core_map: dict[int, int] = {}\n+ filtered_lcores = []\n+ for lcore in lcores_to_filter:\n+ if lcore.core in lcore_count_per_core_map:\n+ current_core_lcore_count = lcore_count_per_core_map[lcore.core]\n+ if self._filter_specifier.lcores_per_core > current_core_lcore_count:\n+ # only add lcores of the given core\n+ lcore_count_per_core_map[lcore.core] += 1\n+ filtered_lcores.append(lcore)\n+ else:\n+ # we have enough lcores per this core\n+ continue\n+ elif self._filter_specifier.cores_per_socket > len(\n+ lcore_count_per_core_map\n+ ):\n+ # only add cores if we need more\n+ lcore_count_per_core_map[lcore.core] = 1\n+ filtered_lcores.append(lcore)\n+ else:\n+ # we have enough cores\n+ break\n+\n+ cores_per_socket = len(lcore_count_per_core_map)\n+ if cores_per_socket < self._filter_specifier.cores_per_socket:\n+ raise ValueError(\n+ f\"The actual number of cores per socket ({cores_per_socket}) \"\n+ f\"is lower than required ({self._filter_specifier.cores_per_socket}).\"\n+ )\n+\n+ lcores_per_core = lcore_count_per_core_map[filtered_lcores[-1].core]\n+ if lcores_per_core < self._filter_specifier.lcores_per_core:\n+ raise ValueError(\n+ f\"The actual number of logical cores per core ({lcores_per_core}) \"\n+ f\"is lower than required ({self._filter_specifier.lcores_per_core}).\"\n+ )\n+\n+ return filtered_lcores\n+\n+\n+class LogicalCoreListFilter(LogicalCoreFilter):\n+ \"\"\"\n+ Filter the input list of Logical Cores according to the input list of\n+ lcore indices.\n+ An empty LogicalCoreList won't filter anything.\n+ \"\"\"\n+\n+ _filter_specifier: LogicalCoreList\n+\n+ def filter(self) -> list[LogicalCore]:\n+ if not len(self._filter_specifier.lcore_list):\n+ return self._lcores_to_filter\n+\n+ filtered_lcores = []\n+ for core in self._lcores_to_filter:\n+ if core.lcore in self._filter_specifier.lcore_list:\n+ filtered_lcores.append(core)\n+\n+ if len(filtered_lcores) != len(self._filter_specifier.lcore_list):\n+ raise ValueError(\n+ f\"Not all logical cores from {self._filter_specifier.lcore_list} \"\n+ f\"were found among {self._lcores_to_filter}\"\n+ )\n+\n+ return filtered_lcores\ndiff --git a/dts/framework/testbed_model/hw/virtual_device.py b/dts/framework/testbed_model/hw/virtual_device.py\nnew file mode 100644\nindex 0000000000..eb664d9f17\n--- /dev/null\n+++ b/dts/framework/testbed_model/hw/virtual_device.py\n@@ -0,0 +1,16 @@\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright(c) 2023 PANTHEON.tech s.r.o.\n+\n+\n+class VirtualDevice(object):\n+ \"\"\"\n+ Base class for virtual devices used by DPDK.\n+ \"\"\"\n+\n+ name: str\n+\n+ def __init__(self, name: str):\n+ self.name = name\n+\n+ def __str__(self) -> str:\n+ return self.name\ndiff --git a/dts/framework/testbed_model/node.py b/dts/framework/testbed_model/node.py\nindex a37f7921e0..b93b9d238e 100644\n--- a/dts/framework/testbed_model/node.py\n+++ b/dts/framework/testbed_model/node.py\n@@ -15,6 +15,14 @@\n from framework.logger import DTSLOG, getLogger\n from framework.remote_session import OSSession, create_session\n \n+from .hw import (\n+ LogicalCore,\n+ LogicalCoreCount,\n+ LogicalCoreList,\n+ LogicalCoreListFilter,\n+ lcore_filter,\n+)\n+\n \n class Node(object):\n \"\"\"\n@@ -26,6 +34,7 @@ class Node(object):\n main_session: OSSession\n config: NodeConfiguration\n name: str\n+ lcores: list[LogicalCore]\n _logger: DTSLOG\n _other_sessions: list[OSSession]\n \n@@ -35,6 +44,12 @@ def __init__(self, node_config: NodeConfiguration):\n self._logger = getLogger(self.name)\n self.main_session = create_session(self.config, self.name, self._logger)\n \n+ self._get_remote_cpus()\n+ # filter the node lcores according to user config\n+ self.lcores = LogicalCoreListFilter(\n+ self.lcores, LogicalCoreList(self.config.lcores)\n+ ).filter()\n+\n self._other_sessions = []\n \n self._logger.info(f\"Created node: {self.name}\")\n@@ -107,6 +122,34 @@ def create_session(self, name: str) -> OSSession:\n self._other_sessions.append(connection)\n return connection\n \n+ def filter_lcores(\n+ self,\n+ filter_specifier: LogicalCoreCount | LogicalCoreList,\n+ ascending: bool = True,\n+ ) -> list[LogicalCore]:\n+ \"\"\"\n+ Filter the LogicalCores found on the Node according to\n+ a LogicalCoreCount or a LogicalCoreList.\n+\n+ If ascending is True, use cores with the lowest numerical id first\n+ and continue in ascending order. If False, start with the highest\n+ id and continue in descending order. This ordering affects which\n+ sockets to consider first as well.\n+ \"\"\"\n+ self._logger.debug(f\"Filtering {filter_specifier} from {self.lcores}.\")\n+ return lcore_filter(\n+ self.lcores,\n+ filter_specifier,\n+ ascending,\n+ ).filter()\n+\n+ def _get_remote_cpus(self) -> None:\n+ \"\"\"\n+ Scan CPUs in the remote OS and store a list of LogicalCores.\n+ \"\"\"\n+ self._logger.info(\"Getting CPU information.\")\n+ self.lcores = self.main_session.get_remote_cpus(self.config.use_first_core)\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 442a41bdc8..a9ae2e4a6f 100644\n--- a/dts/framework/testbed_model/sut_node.py\n+++ b/dts/framework/testbed_model/sut_node.py\n@@ -4,13 +4,16 @@\n \n import os\n import tarfile\n+import time\n from pathlib import PurePath\n \n from framework.config import BuildTargetConfiguration, NodeConfiguration\n+from framework.remote_session import OSSession\n from framework.settings import SETTINGS\n from framework.utils import EnvVarsDict, skip_setup\n \n-from .dpdk import MesonArgs\n+from .dpdk import EalParameters, MesonArgs\n+from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice\n from .node import Node\n \n \n@@ -22,21 +25,29 @@ class SutNode(Node):\n Another key capability is building DPDK according to given build target.\n \"\"\"\n \n+ _dpdk_prefix_list: list[str]\n+ _dpdk_timestamp: str\n _build_target_config: BuildTargetConfiguration | None\n _env_vars: EnvVarsDict\n _remote_tmp_dir: PurePath\n __remote_dpdk_dir: PurePath | None\n _dpdk_version: str | None\n _app_compile_timeout: float\n+ _dpdk_kill_session: OSSession | None\n \n def __init__(self, node_config: NodeConfiguration):\n super(SutNode, self).__init__(node_config)\n+ self._dpdk_prefix_list = []\n self._build_target_config = None\n self._env_vars = EnvVarsDict()\n self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()\n self.__remote_dpdk_dir = None\n self._dpdk_version = None\n self._app_compile_timeout = 90\n+ self._dpdk_kill_session = None\n+ self._dpdk_timestamp = (\n+ f\"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}\"\n+ )\n \n @property\n def _remote_dpdk_dir(self) -> PurePath:\n@@ -169,3 +180,71 @@ def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePa\n return self.main_session.join_remote_path(\n self.remote_dpdk_build_dir, \"examples\", f\"dpdk-{app_name}\"\n )\n+\n+ def kill_cleanup_dpdk_apps(self) -> None:\n+ \"\"\"\n+ Kill all dpdk applications on the SUT. Cleanup hugepages.\n+ \"\"\"\n+ if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():\n+ # we can use the session if it exists and responds\n+ self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)\n+ else:\n+ # otherwise, we need to (re)create it\n+ self._dpdk_kill_session = self.create_session(\"dpdk_kill\")\n+ self._dpdk_prefix_list = []\n+\n+ def create_eal_parameters(\n+ self,\n+ lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),\n+ ascending_cores: bool = True,\n+ prefix: str = \"dpdk\",\n+ append_prefix_timestamp: bool = True,\n+ no_pci: bool = False,\n+ vdevs: list[VirtualDevice] = None,\n+ other_eal_param: str = \"\",\n+ ) -> EalParameters:\n+ \"\"\"\n+ Generate eal parameters character string;\n+ :param lcore_filter_specifier: a number of lcores/cores/sockets to use\n+ or a list of lcore ids to use.\n+ The default will select one lcore for each of two cores\n+ on one socket, in ascending order of core ids.\n+ :param ascending_cores: True, use cores with the lowest numerical id first\n+ and continue in ascending order. If False, start with the\n+ highest id and continue in descending order. This ordering\n+ affects which sockets to consider first as well.\n+ :param prefix: set file prefix string, eg:\n+ prefix='vf'\n+ :param append_prefix_timestamp: if True, will append a timestamp to\n+ DPDK file prefix.\n+ :param no_pci: switch of disable PCI bus eg:\n+ no_pci=True\n+ :param vdevs: virtual device list, eg:\n+ vdevs=['net_ring0', 'net_ring1']\n+ :param other_eal_param: user defined DPDK eal parameters, eg:\n+ other_eal_param='--single-file-segments'\n+ :return: eal param string, eg:\n+ '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420';\n+ \"\"\"\n+\n+ lcore_list = LogicalCoreList(\n+ self.filter_lcores(lcore_filter_specifier, ascending_cores)\n+ )\n+\n+ if append_prefix_timestamp:\n+ prefix = f\"{prefix}_{self._dpdk_timestamp}\"\n+ prefix = self.main_session.get_dpdk_file_prefix(prefix)\n+ if prefix:\n+ self._dpdk_prefix_list.append(prefix)\n+\n+ if vdevs is None:\n+ vdevs = []\n+\n+ return EalParameters(\n+ lcore_list=lcore_list,\n+ memory_channels=self.config.memory_channels,\n+ prefix=prefix,\n+ no_pci=no_pci,\n+ vdevs=vdevs,\n+ other_eal_param=other_eal_param,\n+ )\ndiff --git a/dts/framework/utils.py b/dts/framework/utils.py\nindex 611071604b..eebe76f16c 100644\n--- a/dts/framework/utils.py\n+++ b/dts/framework/utils.py\n@@ -32,6 +32,26 @@ def skip_setup(func) -> Callable[..., None]:\n return func\n \n \n+def expand_range(range_str: str) -> list[int]:\n+ \"\"\"\n+ Process range string into a list of integers. There are two possible formats:\n+ n - a single integer\n+ n-m - a range of integers\n+\n+ The returned range includes both n and m. Empty string returns an empty list.\n+ \"\"\"\n+ expanded_range: list[int] = []\n+ if range_str:\n+ range_boundaries = range_str.split(\"-\")\n+ # will throw an exception when items in range_boundaries can't be converted,\n+ # serving as type check\n+ expanded_range.extend(\n+ range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)\n+ )\n+\n+ return expanded_range\n+\n+\n def GREEN(text: str) -> str:\n return f\"\\u001B[32;1m{str(text)}\\u001B[0m\"\n \n", "prefixes": [ "v5", "04/10" ] }{ "id": 124465, "url": "