From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 12802A055F; Wed, 16 Nov 2022 14:29:25 +0100 (CET) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id AFD3F40E03; Wed, 16 Nov 2022 14:29:24 +0100 (CET) Received: from mail-pj1-f51.google.com (mail-pj1-f51.google.com [209.85.216.51]) by mails.dpdk.org (Postfix) with ESMTP id 567B240DFB for ; Wed, 16 Nov 2022 14:29:23 +0100 (CET) Received: by mail-pj1-f51.google.com with SMTP id m14-20020a17090a3f8e00b00212dab39bcdso2369926pjc.0 for ; Wed, 16 Nov 2022 05:29:23 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=x9c2Lr5MpDF4N356KEHKeuq7wDx8+VD3uGYCymMaXmg=; b=N3DgLAh1NftRFrqPyqS7nwf2Qe+0Xwz34/wSn+SqJ4lPU4HnEzKgEogw4E4oRVUl6P wWkyCR54ggfmRGnUwTeDQh+vFu0RYjR7gmj/QyuncQ+Qzb/RNhzMGXrwHln62jJjmvXo xiytM64y4lI04ywQLKVHF8LKFrb426NfEuGlc= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=x9c2Lr5MpDF4N356KEHKeuq7wDx8+VD3uGYCymMaXmg=; b=FJA90+FLGJ1J3jHnrYAVKNO5mxUNZn2ztibFKRzfma1vZ2B3d7dGuaHJ+IS/ksmv48 XivpPj6s5pi9lp//yVKMYDPdyBBcqo4/t7l448K6sPjgw4BTWVt4k6VOamWgpv4vDTKn H0MX2XG/bQ6AGjxlZCjCfl09A1tMG0ZVv8emAbh7Tr0NkHuvLW+KkEzd7ATSWDHcvP6H tqZToSKL25VnWMxWkyfXcLZH0tPxfeBAjBM2fejgkC5vQqewdHgSzH/5+CqbjKcSBG9u b3gCzV6q2dmR8Yknt4R9Dq0cps6Pq+7se2uVXO12RMA/rXpcQZnQ/IxL1ezpGVLMQyS3 6oBA== X-Gm-Message-State: ANoB5pk7jO1o6yb+r1mbFo6pWjiPSrolDYd7vcwbuuS5RC3HROE24KhV chpBN+hXf/cnHoWl+PuscI7oDh35OFaVtaXtP0Ca3Q== X-Google-Smtp-Source: AA0mqf4w8gGSLI7kO45zQxvMSHyhWk7wQPunEyGrhOVZzuAg6AQ736mzjcGSNdQO0zW31ZZQBVTdq9rfexDySHxx7EI= X-Received: by 2002:a17:90a:4147:b0:214:2214:869e with SMTP id m7-20020a17090a414700b002142214869emr3936522pjg.76.1668605362295; Wed, 16 Nov 2022 05:29:22 -0800 (PST) MIME-Version: 1.0 References: <20220824162454.394285-1-juraj.linkes@pantheon.tech> <20221114165438.1133783-1-juraj.linkes@pantheon.tech> <20221114165438.1133783-5-juraj.linkes@pantheon.tech> In-Reply-To: <20221114165438.1133783-5-juraj.linkes@pantheon.tech> From: Owen Hilyard Date: Wed, 16 Nov 2022 08:28:46 -0500 Message-ID: Subject: Re: [RFC PATCH v2 04/10] dts: add dpdk execution handling To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, bruce.richardson@intel.com, dev@dpdk.org Content-Type: multipart/alternative; boundary="000000000000e3f07c05ed967515" 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 --000000000000e3f07c05ed967515 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Mon, Nov 14, 2022 at 11:54 AM Juraj Linke=C5=A1 wrote: > Add methods for setting up and shutting down DPDK apps and for > constructing EAL parameters. > > Signed-off-by: Juraj Linke=C5=A1 > --- > dts/conf.yaml | 4 + > dts/framework/config/__init__.py | 85 ++++++++- > dts/framework/config/conf_yaml_schema.json | 22 +++ > .../remote_session/os/linux_session.py | 15 ++ > dts/framework/remote_session/os/os_session.py | 16 +- > .../remote_session/os/posix_session.py | 80 ++++++++ > dts/framework/testbed_model/hw/__init__.py | 17 ++ > dts/framework/testbed_model/hw/cpu.py | 164 ++++++++++++++++ > dts/framework/testbed_model/node/node.py | 36 ++++ > dts/framework/testbed_model/node/sut_node.py | 178 +++++++++++++++++- > dts/framework/utils.py | 20 ++ > 11 files changed, 634 insertions(+), 3 deletions(-) > create mode 100644 dts/framework/testbed_model/hw/__init__.py > create mode 100644 dts/framework/testbed_model/hw/cpu.py > > diff --git a/dts/conf.yaml b/dts/conf.yaml > index 6b0bc5c2bf..976888a88e 100644 > --- a/dts/conf.yaml > +++ b/dts/conf.yaml > @@ -12,4 +12,8 @@ nodes: > - name: "SUT 1" > hostname: sut1.change.me.localhost > user: root > + arch: x86_64 > os: linux > + bypass_core0: true > + cpus: "" > + memory_channels: 4 > diff --git a/dts/framework/config/__init__.py > b/dts/framework/config/__init__.py > index 1b97dc3ab9..344d697a69 100644 > --- a/dts/framework/config/__init__.py > +++ b/dts/framework/config/__init__.py > @@ -11,12 +11,13 @@ > import pathlib > from dataclasses import dataclass > from enum import Enum, auto, unique > -from typing import Any > +from typing import Any, Iterable > > import warlock # type: ignore > import yaml > > from framework.settings import SETTINGS > +from framework.utils import expand_range > > > class StrEnum(Enum): > @@ -60,6 +61,80 @@ class Compiler(StrEnum): > msvc =3D auto() > > > +@dataclass(slots=3DTrue, frozen=3DTrue) > +class CPU: > + cpu: int > + core: int > + socket: int > + node: int > + > + def __str__(self) -> str: > + return str(self.cpu) > + > + > +class CPUList(object): > + """ > + Convert these options into a list of int cpus > + cpu_list=3D[CPU1, CPU2] - a list of CPUs > + cpu_list=3D[0,1,2,3] - a list of int indices > + cpu_list=3D['0','1','2-3'] - a list of str indices; ranges are suppo= rted > + cpu_list=3D'0,1,2-3' - a comma delimited str of indices; ranges are > supported > + > + The class creates a unified format used across the framework and > allows > + the user to use either a str representation (using str(instance) or > directly > + in f-strings) or a list representation (by accessing > instance.cpu_list). > + Empty cpu_list is allowed. > + """ > + > + _cpu_list: list[int] > + > + def __init__(self, cpu_list: list[int | str | CPU] | str): > + self._cpu_list =3D [] > + if isinstance(cpu_list, str): > + self._from_str(cpu_list.split(",")) > + else: > + self._from_str((str(cpu) for cpu in cpu_list)) > + > + # the input cpus may not be sorted > + self._cpu_list.sort() > + > + @property > + def cpu_list(self) -> list[int]: > + return self._cpu_list > + > + def _from_str(self, cpu_list: Iterable[str]) -> None: > + for cpu in cpu_list: > + self._cpu_list.extend(expand_range(cpu)) > + > + def _get_consecutive_cpus_range(self, cpu_list: list[int]) -> > list[str]: > + formatted_core_list =3D [] > + tmp_cpus_list =3D list(sorted(cpu_list)) > + segment =3D tmp_cpus_list[:1] > + for core_id in tmp_cpus_list[1:]: > + if core_id - segment[-1] =3D=3D 1: > + segment.append(core_id) > + else: > + formatted_core_list.append( > + f"{segment[0]}-{segment[-1]}" > + if len(segment) > 1 > + else f"{segment[0]}" > + ) > + current_core_index =3D tmp_cpus_list.index(core_id) > + formatted_core_list.extend( > + > self._get_consecutive_cpus_range(tmp_cpus_list[current_core_index:]) > + ) > + segment.clear() > + break > + if len(segment) > 0: > + formatted_core_list.append( > + f"{segment[0]}-{segment[-1]}" if len(segment) > 1 else > f"{segment[0]}" > + ) > + return formatted_core_list > + > + def __str__(self) -> str: > + return > f'{",".join(self._get_consecutive_cpus_range(self._cpu_list))}' > + > + > # Slots enables some optimizations, by pre-allocating space for the > defined > # attributes in the underlying data structure. > # > @@ -71,7 +146,11 @@ class NodeConfiguration: > hostname: str > user: str > password: str | None > + arch: Architecture > os: OS > + bypass_core0: bool > + cpus: CPUList > + memory_channels: int > > @staticmethod > def from_dict(d: dict) -> "NodeConfiguration": > @@ -80,7 +159,11 @@ def from_dict(d: dict) -> "NodeConfiguration": > hostname=3Dd["hostname"], > user=3Dd["user"], > password=3Dd.get("password"), > + arch=3DArchitecture(d["arch"]), > os=3DOS(d["os"]), > + bypass_core0=3Dd.get("bypass_core0", False), > + cpus=3DCPUList(d.get("cpus", "1")), > + memory_channels=3Dd.get("memory_channels", 1), > ) > > > diff --git a/dts/framework/config/conf_yaml_schema.json > b/dts/framework/config/conf_yaml_schema.json > index 409ce7ac74..c59d3e30e6 100644 > --- a/dts/framework/config/conf_yaml_schema.json > +++ b/dts/framework/config/conf_yaml_schema.json > @@ -6,6 +6,12 @@ > "type": "string", > "description": "A unique identifier for a node" > }, > + "ARCH": { > + "type": "string", > + "enum": [ > + "x86_64" > arm64 and ppc64le should probably be included here. I think that we can focus on 64 bit arches for now. > + ] > + }, > "OS": { > "type": "string", > "enum": [ > @@ -82,8 +88,23 @@ > "type": "string", > "description": "The password to use on this node. Use only a= s > a last resort. SSH keys are STRONGLY preferred." > }, > + "arch": { > + "$ref": "#/definitions/ARCH" > + }, > "os": { > "$ref": "#/definitions/OS" > + }, > + "bypass_core0": { > + "type": "boolean", > + "description": "Indicate that DPDK should omit using the > first core." > + }, > + "cpus": { > + "type": "string", > + "description": "Optional comma-separated list of cpus to use= , > e.g.: 1,2,3,4,5,18-22. Defaults to 1. An empty string means use all cpus.= " > + }, > + "memory_channels": { > + "type": "integer", > + "description": "How many memory channels to use. Optional, > defaults to 1." > } > }, > "additionalProperties": false, > @@ -91,6 +112,7 @@ > "name", > "hostname", > "user", > + "arch", > "os" > ] > }, > diff --git a/dts/framework/remote_session/os/linux_session.py > b/dts/framework/remote_session/os/linux_session.py > index 39e80631dd..21f117b714 100644 > --- a/dts/framework/remote_session/os/linux_session.py > +++ b/dts/framework/remote_session/os/linux_session.py > @@ -2,6 +2,8 @@ > # Copyright(c) 2022 PANTHEON.tech s.r.o. > # Copyright(c) 2022 University of New Hampshire > > +from framework.config import CPU > + > from .posix_session import PosixSession > > > @@ -9,3 +11,16 @@ class LinuxSession(PosixSession): > """ > The implementation of non-Posix compliant parts of Linux remote > sessions. > """ > + > + def get_remote_cpus(self, bypass_core0: bool) -> list[CPU]: > + cpu_info =3D self.remote_session.send_command( > + "lscpu -p=3DCPU,CORE,SOCKET,NODE|grep -v \\#" > + ).stdout > + cpus =3D [] > + for cpu_line in cpu_info.splitlines(): > + cpu, core, socket, node =3D cpu_line.split(",") > + if bypass_core0 and core =3D=3D 0 and socket =3D=3D 0: > + self.logger.info("Core0 bypassed.") > + continue > + cpus.append(CPU(int(cpu), int(core), int(socket), int(node))= ) > + return cpus > diff --git a/dts/framework/remote_session/os/os_session.py > b/dts/framework/remote_session/os/os_session.py > index 57e2865282..6f6b6a979e 100644 > --- a/dts/framework/remote_session/os/os_session.py > +++ b/dts/framework/remote_session/os/os_session.py > @@ -3,9 +3,10 @@ > # Copyright(c) 2022 University of New Hampshire > > from abc import ABC, abstractmethod > +from collections.abc import Iterable > from pathlib import PurePath > > -from framework.config import Architecture, NodeConfiguration > +from framework.config import CPU, Architecture, NodeConfiguration > from framework.logger import DTSLOG > from framework.remote_session.factory import create_remote_session > from framework.remote_session.remote_session import RemoteSession > @@ -130,3 +131,16 @@ def get_dpdk_version(self, version_path: str | > PurePath) -> str: > """ > Inspect DPDK version on the remote node from version_path. > """ > + > + @abstractmethod > + def get_remote_cpus(self, bypass_core0: bool) -> list[CPU]: > + """ > + Compose a list of CPUs present on the remote node. > + """ > + > + @abstractmethod > + def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> > None: > + """ > + Kill and cleanup all DPDK apps identified by dpdk_prefix_list. I= f > + dpdk_prefix_list is empty, attempt to find running DPDK apps to > kill and clean. > + """ > diff --git a/dts/framework/remote_session/os/posix_session.py > b/dts/framework/remote_session/os/posix_session.py > index a36b8e8c1a..7151263c7a 100644 > --- a/dts/framework/remote_session/os/posix_session.py > +++ b/dts/framework/remote_session/os/posix_session.py > @@ -2,6 +2,8 @@ > # Copyright(c) 2022 PANTHEON.tech s.r.o. > # Copyright(c) 2022 University of New Hampshire > > +import re > +from collections.abc import Iterable > from pathlib import PurePath, PurePosixPath > > from framework.config import Architecture > @@ -138,3 +140,81 @@ def get_dpdk_version(self, build_dir: str | PurePath= ) > -> str: > f"cat {self.join_remote_path(build_dir, 'VERSION')}", > verify=3DTrue > ) > return out.stdout > + > + def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> > None: > + self.logger.info("Cleaning up DPDK apps.") > + dpdk_runtime_dirs =3D self._get_dpdk_runtime_dirs(dpdk_prefix_li= st) > + if dpdk_runtime_dirs: > + # kill and cleanup only if DPDK is running > + dpdk_pids =3D self._get_dpdk_pids(dpdk_runtime_dirs) > + for dpdk_pid in dpdk_pids: > + self.remote_session.send_command(f"kill -9 {dpdk_pid}", > 20) > + self._check_dpdk_hugepages(dpdk_runtime_dirs) > + self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs) > + > + def _get_dpdk_runtime_dirs( > + self, dpdk_prefix_list: Iterable[str] > + ) -> list[PurePosixPath]: > + prefix =3D PurePosixPath("/var", "run", "dpdk") > + if not dpdk_prefix_list: > + remote_prefixes =3D self._list_remote_dirs(prefix) > + if not remote_prefixes: > + dpdk_prefix_list =3D [] > + else: > + dpdk_prefix_list =3D remote_prefixes > + > + return [PurePosixPath(prefix, dpdk_prefix) for dpdk_prefix in > dpdk_prefix_list] > + > + def _list_remote_dirs(self, remote_path: str | PurePath) -> list[str= ] > | None: > + """ > + Return a list of directories of the remote_dir. > + If remote_path doesn't exist, return None. > + """ > + out =3D self.remote_session.send_command( > + f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'" > + ).stdout > + if "No such file or directory" in out: > + return None > + else: > + return out.splitlines() > + > + def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[str | PurePath]= ) > -> list[int]: > + pids =3D [] > + pid_regex =3D r"p(\d+)" > + for dpdk_runtime_dir in dpdk_runtime_dirs: > + dpdk_config_file =3D PurePosixPath(dpdk_runtime_dir, "config= ") > + if self._remote_files_exists(dpdk_config_file): > + out =3D self.remote_session.send_command( > + f"lsof -Fp {dpdk_config_file}" > + ).stdout > + if out and "No such file or directory" not in out: > + for out_line in out.splitlines(): > + match =3D re.match(pid_regex, out_line) > + if match: > + pids.append(int(match.group(1))) > + return pids > + > + def _remote_files_exists(self, remote_path: PurePath) -> bool: > + result =3D self.remote_session.send_command(f"test -e > {remote_path}") > + return not result.return_code > + > + def _check_dpdk_hugepages( > + self, dpdk_runtime_dirs: Iterable[str | PurePath] > + ) -> None: > + for dpdk_runtime_dir in dpdk_runtime_dirs: > + hugepage_info =3D PurePosixPath(dpdk_runtime_dir, > "hugepage_info") > + if self._remote_files_exists(hugepage_info): > + out =3D self.remote_session.send_command( > + f"lsof -Fp {hugepage_info}" > + ).stdout > + if out and "No such file or directory" not in out: > + self.logger.warning("Some DPDK processes did not fre= e > hugepages.") > + > self.logger.warning("*******************************************") > + self.logger.warning(out) > + > self.logger.warning("*******************************************") > + > + def _remove_dpdk_runtime_dirs( > + self, dpdk_runtime_dirs: Iterable[str | PurePath] > + ) -> None: > + for dpdk_runtime_dir in dpdk_runtime_dirs: > + self.remove_remote_dir(dpdk_runtime_dir) > diff --git a/dts/framework/testbed_model/hw/__init__.py > b/dts/framework/testbed_model/hw/__init__.py > new file mode 100644 > index 0000000000..7d79a7efd0 > --- /dev/null > +++ b/dts/framework/testbed_model/hw/__init__.py > @@ -0,0 +1,17 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > + > +from framework.config import CPU, CPUList > + > +from .cpu import CPUAmount, CPUAmountFilter, CPUFilter, CPUListFilter > + > + > +def cpu_filter( > + core_list: list[CPU], filter_specifier: CPUAmount | CPUList, > ascending: bool > +) -> CPUFilter: > + if isinstance(filter_specifier, CPUList): > + return CPUListFilter(core_list, filter_specifier, ascending) > + elif isinstance(filter_specifier, CPUAmount): > + return CPUAmountFilter(core_list, filter_specifier, ascending) > + else: > + raise ValueError(f"Unsupported filter r{filter_specifier}") > diff --git a/dts/framework/testbed_model/hw/cpu.py > b/dts/framework/testbed_model/hw/cpu.py > new file mode 100644 > index 0000000000..87e87bcb4e > --- /dev/null > +++ b/dts/framework/testbed_model/hw/cpu.py > @@ -0,0 +1,164 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > + > +import dataclasses > +from abc import ABC, abstractmethod > +from collections.abc import Iterable > + > +from framework.config import CPU, CPUList > + > + > +@dataclasses.dataclass(slots=3DTrue, frozen=3DTrue) > +class CPUAmount: > + """ > + Define the amounts of cpus to use. If sockets is not None, > socket_amount > + is ignored. > + """ > + > + cpus_per_core: int =3D 1 > + cores_per_socket: int =3D 2 > + socket_amount: int =3D 1 > + sockets: list[int] | None =3D None > + > + > +class CPUFilter(ABC): > + """ > + Filter according to the input filter specifier. Each filter needs to > be > + implemented in a derived class. > + This class only implements operations common to all filters, such as > sorting > + the list to be filtered beforehand. > + """ > + > + _filter_specifier: CPUAmount | CPUList > + _cpus_to_filter: list[CPU] > + > + def __init__( > + self, > + core_list: list[CPU], > + filter_specifier: CPUAmount | CPUList, > + ascending: bool =3D True, > + ) -> None: > + self._filter_specifier =3D filter_specifier > + > + # sorting by core is needed in case hyperthreading is enabled > + self._cpus_to_filter =3D sorted( > + core_list, key=3Dlambda x: x.core, reverse=3Dnot ascending > + ) > + self.filter() > + > + @abstractmethod > + def filter(self) -> list[CPU]: > + """ > + Use the input self._filter_specifier to filter > self._cpus_to_filter > + and return the list of filtered CPUs. self._cpus_to_filter is a > + sorter copy of the original list, so it may be modified. > + """ > + > + > +class CPUAmountFilter(CPUFilter): > + """ > + Filter the input list of CPUs according to specified rules: > + Use cores from the specified amount of sockets or from the specified > socket ids. > + If sockets is specified, it takes precedence over socket_amount. > + From each of those sockets, use only cores_per_socket of cores. > + And for each core, use cpus_per_core of cpus. Hypertheading > + must be enabled for this to take effect. > + If ascending is True, use cores with the lowest numerical id first > + and continue in ascending order. If False, start with the highest > + id and continue in descending order. This ordering affects which > + sockets to consider first as well. > + """ > + > + _filter_specifier: CPUAmount > + > + def filter(self) -> list[CPU]: > + return > self._filter_cpus(self._filter_sockets(self._cpus_to_filter)) > + > + def _filter_sockets(self, cpus_to_filter: Iterable[CPU]) -> list[CPU= ]: > + allowed_sockets: set[int] =3D set() > + socket_amount =3D self._filter_specifier.socket_amount > + if self._filter_specifier.sockets: > + socket_amount =3D len(self._filter_specifier.sockets) > + allowed_sockets =3D set(self._filter_specifier.sockets) > + > + filtered_cpus =3D [] > + for cpu in cpus_to_filter: > + if not self._filter_specifier.sockets: > + if len(allowed_sockets) < socket_amount: > + allowed_sockets.add(cpu.socket) > + if cpu.socket in allowed_sockets: > + filtered_cpus.append(cpu) > + > + if len(allowed_sockets) < socket_amount: > + raise ValueError( > + f"The amount of sockets from which to use cores " > + f"({socket_amount}) exceeds the actual amount present " > + f"on the node ({len(allowed_sockets)})" > + ) > + > + return filtered_cpus > + > + def _filter_cpus(self, cpus_to_filter: Iterable[CPU]) -> list[CPU]: > + # no need to use ordered dict, from Python3.7 the dict > + # insertion order is preserved (LIFO). > + allowed_cpu_per_core_count_map: dict[int, int] =3D {} > + filtered_cpus =3D [] > + for cpu in cpus_to_filter: > + if cpu.core in allowed_cpu_per_core_count_map: > + cpu_count =3D allowed_cpu_per_core_count_map[cpu.core] > + if self._filter_specifier.cpus_per_core > cpu_count: > + # only add cpus of the given core > + allowed_cpu_per_core_count_map[cpu.core] +=3D 1 > + filtered_cpus.append(cpu) > + else: > + raise ValueError( > + f"The amount of CPUs per core to use " > + f"({self._filter_specifier.cpus_per_core}) " > + f"exceeds the actual amount present. Is > hyperthreading enabled?" > + ) > + elif self._filter_specifier.cores_per_socket > len( > + allowed_cpu_per_core_count_map > + ): > + # only add cpus if we need more > + allowed_cpu_per_core_count_map[cpu.core] =3D 1 > + filtered_cpus.append(cpu) > + else: > + # cpus are sorted by core, at this point we won't > encounter new cores > + break > + > + cores_per_socket =3D len(allowed_cpu_per_core_count_map) > + if cores_per_socket < self._filter_specifier.cores_per_socket: > + raise ValueError( > + f"The amount of cores per socket to use " > + f"({self._filter_specifier.cores_per_socket}) " > + f"exceeds the actual amount present ({cores_per_socket})= " > + ) > + > + return filtered_cpus > + > + > +class CPUListFilter(CPUFilter): > + """ > + Filter the input list of CPUs according to the input list of > + core indices. > + An empty CPUList won't filter anything. > + """ > + > + _filter_specifier: CPUList > + > + def filter(self) -> list[CPU]: > + if not len(self._filter_specifier.cpu_list): > + return self._cpus_to_filter > + > + filtered_cpus =3D [] > + for core in self._cpus_to_filter: > + if core.cpu in self._filter_specifier.cpu_list: > + filtered_cpus.append(core) > + > + if len(filtered_cpus) !=3D len(self._filter_specifier.cpu_list): > + raise ValueError( > + f"Not all cpus from {self._filter_specifier.cpu_list} > were found" > + f"among {self._cpus_to_filter}" > + ) > + > + return filtered_cpus > diff --git a/dts/framework/testbed_model/node/node.py > b/dts/framework/testbed_model/node/node.py > index 86654e55ae..5ee7023335 100644 > --- a/dts/framework/testbed_model/node/node.py > +++ b/dts/framework/testbed_model/node/node.py > @@ -8,13 +8,16 @@ > """ > > from framework.config import ( > + CPU, > BuildTargetConfiguration, > + CPUList, > ExecutionConfiguration, > NodeConfiguration, > ) > from framework.exception import NodeCleanupError, NodeSetupError, > convert_exception > from framework.logger import DTSLOG, getLogger > from framework.remote_session import OSSession, create_session > +from framework.testbed_model.hw import CPUAmount, cpu_filter > > > class Node(object): > @@ -28,6 +31,7 @@ class Node(object): > main_session: OSSession > logger: DTSLOG > config: NodeConfiguration > + cpus: list[CPU] > _other_sessions: list[OSSession] > > def __init__(self, node_config: NodeConfiguration): > @@ -38,6 +42,7 @@ def __init__(self, node_config: NodeConfiguration): > self.logger =3D getLogger(self.name) > self.logger.info(f"Created node: {self.name}") > self.main_session =3D create_session(self.config, self.name, > self.logger) > + self._get_remote_cpus() > > @convert_exception(NodeSetupError) > def setup_execution(self, execution_config: ExecutionConfiguration) > -> None: > @@ -109,6 +114,37 @@ def create_session(self, name: str) -> OSSession: > self._other_sessions.append(connection) > return connection > > + def filter_cpus( > + self, > + filter_specifier: CPUAmount | CPUList, > + ascending: bool =3D True, > + ) -> list[CPU]: > + """ > + Filter the logical cpus found on the Node according to specified > rules: > + Use cores from the specified amount of sockets or from the > specified > + socket ids. If sockets is specified, it takes precedence over > socket_amount. > + From each of those sockets, use only cpus_per_socket of cores. > + And for each core, use cpus_per_core of cpus. Hypertheading > + must be enabled for this to take effect. > + If ascending is True, use cores with the lowest numerical id fir= st > + and continue in ascending order. If False, start with the highes= t > + id and continue in descending order. This ordering affects which > + sockets to consider first as well. > + """ > + self.logger.info("Filtering ") > + return cpu_filter( > + self.cpus, > + filter_specifier, > + ascending, > + ).filter() > + > + def _get_remote_cpus(self) -> None: > + """ > + Scan cpus in the remote OS and store a list of CPUs. > + """ > + self.logger.info("Getting CPU information.") > + self.cpus =3D > self.main_session.get_remote_cpus(self.config.bypass_core0) > + > def close(self) -> None: > """ > Close all connections and free other resources. > diff --git a/dts/framework/testbed_model/node/sut_node.py > b/dts/framework/testbed_model/node/sut_node.py > index 53268a7565..ff3be845b4 100644 > --- a/dts/framework/testbed_model/node/sut_node.py > +++ b/dts/framework/testbed_model/node/sut_node.py > @@ -4,10 +4,13 @@ > > import os > import tarfile > +import time > from pathlib import PurePath > > -from framework.config import BuildTargetConfiguration, NodeConfiguration > +from framework.config import CPU, BuildTargetConfiguration, CPUList, > NodeConfiguration > +from framework.remote_session import OSSession > from framework.settings import SETTINGS > +from framework.testbed_model.hw import CPUAmount, CPUListFilter > from framework.utils import EnvVarsDict, skip_setup > > from .node import Node > @@ -21,19 +24,31 @@ class SutNode(Node): > Another key capability is building DPDK according to given build > target. > """ > > + cpus: list[CPU] > + dpdk_prefix_list: list[str] > + dpdk_prefix_subfix: str > _build_target_config: BuildTargetConfiguration | None > _env_vars: EnvVarsDict > _remote_tmp_dir: PurePath > __remote_dpdk_dir: PurePath | None > _app_compile_timeout: float > + _dpdk_kill_session: OSSession | None > > def __init__(self, node_config: NodeConfiguration): > super(SutNode, self).__init__(node_config) > + self.dpdk_prefix_list =3D [] > self._build_target_config =3D None > self._env_vars =3D EnvVarsDict() > self._remote_tmp_dir =3D self.main_session.get_remote_tmp_dir() > self.__remote_dpdk_dir =3D None > self._app_compile_timeout =3D 90 > + self._dpdk_kill_session =3D None > + > + # filter the node cpus according to user config > + self.cpus =3D CPUListFilter(self.cpus, self.config.cpus).filter(= ) > + self.dpdk_prefix_subfix =3D ( > + f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', > time.localtime())}" > + ) > > @property > def _remote_dpdk_dir(self) -> PurePath: > @@ -142,3 +157,164 @@ def build_dpdk_app(self, app_name: str) -> PurePath= : > return self.main_session.join_remote_path( > build_dir, "examples", f"dpdk-{app_name}" > ) > + > + def kill_cleanup_dpdk_apps(self) -> None: > + """ > + Kill all dpdk applications on the SUT. Cleanup hugepages. > + """ > + if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(= ): > + # we can use the session if it exists and responds > + > self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list) > + else: > + # otherwise, we need to (re)create it > + self._dpdk_kill_session =3D self.create_session("dpdk_kill") > + self.dpdk_prefix_list =3D [] > + > + def create_eal_parameters( > + self, > + fixed_prefix: bool =3D False, > + core_filter_specifier: CPUAmount | CPUList =3D CPUAmount(), > + ascending_cores: bool =3D True, > + prefix: str =3D "", > + no_pci: bool =3D False, > + vdevs: list[str] =3D None, > I would prefer to have vdevs be a list of objects, even if for now that class just takes a string in its constructor. Later on we can add subclasses for specific vdevs that might see heavy use, such as librte_net_pcap and crypto_openssl. > + other_eal_param: str =3D "", > + ) -> str: > + """ > + Generate eal parameters character string; > + :param fixed_prefix: use fixed file-prefix or not, when it is > true, > + the file-prefix will not be added a timestamp > + :param core_filter_specifier: an amount of cpus/cores/sockets to > use > + or a list of cpu ids to use. > + The default will select one cpu for each of two > cores > + on one socket, in ascending order of core ids. > + :param ascending_cores: True, use cores with the lowest numerica= l > id first > + and continue in ascending order. If False, start > with the > + highest id and continue in descending order. Thi= s > ordering > + affects which sockets to consider first as well. > + :param prefix: set file prefix string, eg: > + prefix=3D'vf'; > + :param no_pci: switch of disable PCI bus eg: > + no_pci=3DTrue; > + :param vdevs: virtual device list, eg: > + vdevs=3D['net_ring0', 'net_ring1']; > + :param other_eal_param: user defined DPDK eal parameters, eg: > + other_eal_param=3D'--single-file-segments'; > + :return: eal param string, eg: > + '-c 0xf -a 0000:88:00.0 > --file-prefix=3Ddpdk_1112_20190809143420'; > + if DPDK version < 20.11-rc4, eal_str eg: > + '-c 0xf -w 0000:88:00.0 > --file-prefix=3Ddpdk_1112_20190809143420'; > + """ > + if vdevs is None: > + vdevs =3D [] > + > + config =3D { > + "core_filter_specifier": core_filter_specifier, > + "ascending_cores": ascending_cores, > + "prefix": prefix, > + "no_pci": no_pci, > + "vdevs": vdevs, > + "other_eal_param": other_eal_param, > + } > + > + eal_parameter_creator =3D _EalParameter( > + sut_node=3Dself, fixed_prefix=3Dfixed_prefix, **config > + ) > + eal_str =3D eal_parameter_creator.make_eal_param() > + > + return eal_str > + > + > +class _EalParameter(object): > + def __init__( > + self, > + sut_node: SutNode, > + fixed_prefix: bool, > + core_filter_specifier: CPUAmount | CPUList, > + ascending_cores: bool, > + prefix: str, > + no_pci: bool, > + vdevs: list[str], > + other_eal_param: str, > + ): > + """ > + Generate eal parameters character string; > + :param sut_node: SUT Node; > + :param fixed_prefix: use fixed file-prefix or not, when it is > true, > + he file-prefix will not be added a timestamp > + :param core_filter_specifier: an amount of cpus/cores/sockets to > use > + or a list of cpu ids to use. > + :param ascending_cores: True, use cores with the lowest numerica= l > id first > + and continue in ascending order. If False, start > with the > + highest id and continue in descending order. Thi= s > ordering > + affects which sockets to consider first as well. > + :param prefix: set file prefix string, eg: > + prefix=3D'vf'; > + :param no_pci: switch of disable PCI bus eg: > + no_pci=3DTrue; > + :param vdevs: virtual device list, eg: > + vdevs=3D['net_ring0', 'net_ring1']; > + :param other_eal_param: user defined DPDK eal parameters, eg: > + other_eal_param=3D'--single-file-segments'; > + """ > + self.os =3D sut_node.config.os > + self.fixed_prefix =3D fixed_prefix > + self.sut_node =3D sut_node > + self.core_filter_specifier =3D core_filter_specifier > + self.ascending_cores =3D ascending_cores > + self.prefix =3D prefix > + self.no_pci =3D no_pci > + self.vdevs =3D vdevs > + self.other_eal_param =3D other_eal_param > + > + def _make_lcores_param(self) -> str: > + filtered_cpus =3D self.sut_node.filter_cpus( > + self.core_filter_specifier, self.ascending_cores > + ) > + return f"-l {CPUList(filtered_cpus)}" > + > + def _make_memory_channels(self) -> str: > + param_template =3D "-n {}" > + return param_template.format(self.sut_node.config.memory_channel= s) > + > + def _make_no_pci_param(self) -> str: > + if self.no_pci is True: > + return "--no-pci" > + else: > + return "" > + > + def _make_prefix_param(self) -> str: > + if self.prefix =3D=3D "": > + fixed_file_prefix =3D f"dpdk_{self.sut_node.dpdk_prefix_subf= ix}" > + else: > + fixed_file_prefix =3D self.prefix > + if not self.fixed_prefix: > + fixed_file_prefix =3D ( > + > f"{fixed_file_prefix}_{self.sut_node.dpdk_prefix_subfix}" > + ) > + fixed_file_prefix =3D > self._do_os_handle_with_prefix_param(fixed_file_prefix) > + return fixed_file_prefix > + > + def _make_vdevs_param(self) -> str: > + if len(self.vdevs) =3D=3D 0: > + return "" > + else: > + return " ".join(f"--vdev {vdev}" for vdev in self.vdevs) > + > + def _do_os_handle_with_prefix_param(self, file_prefix: str) -> str: > + self.sut_node.dpdk_prefix_list.append(file_prefix) > + return f"--file-prefix=3D{file_prefix}" > + > + def make_eal_param(self) -> str: > + _eal_str =3D " ".join( > + [ > + self._make_lcores_param(), > + self._make_memory_channels(), > + self._make_prefix_param(), > + self._make_no_pci_param(), > + self._make_vdevs_param(), > + # append user defined eal parameters > + self.other_eal_param, > + ] > + ) > + return _eal_str > diff --git a/dts/framework/utils.py b/dts/framework/utils.py > index 91e58f3218..3c2f0adff9 100644 > --- a/dts/framework/utils.py > +++ b/dts/framework/utils.py > @@ -32,6 +32,26 @@ def skip_setup(func) -> Callable[..., None]: > return func > > > +def expand_range(range_str: str) -> list[int]: > + """ > + Process range string into a list of integers. There are two possible > formats: > + n - a single integer > + n-m - a range of integers > + > + The returned range includes both n and m. Empty string returns an > empty list. > + """ > + expanded_range: list[int] =3D [] > + if range_str: > + range_boundaries =3D range_str.split("-") > + # will throw an exception when items in range_boundaries can't b= e > converted, > + # serving as type check > + expanded_range.extend( > + range(int(range_boundaries[0]), int(range_boundaries[-1]) + = 1) > + ) > + > + return expanded_range > + > + > def GREEN(text: str) -> str: > return f"\u001B[32;1m{str(text)}\u001B[0m" > > -- > 2.30.2 > > --000000000000e3f07c05ed967515 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
On Mon, Nov 14, 2022 at 11:54 AM Juraj Li= nke=C5=A1 <juraj.linkes@pantheon.tech> wrote:
Add methods = for setting up and shutting down DPDK apps and for
constructing EAL parameters.

Signed-off-by: Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech>
---
=C2=A0dts/conf.yaml=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2=A0 =C2= =A04 +
=C2=A0dts/framework/config/__init__.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 |=C2=A0 85 ++++++++-
=C2=A0dts/framework/config/conf_yaml_schema.json=C2=A0 =C2=A0 |=C2=A0 22 ++= +
=C2=A0.../remote_session/os/linux_session.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 |= =C2=A0 15 ++
=C2=A0dts/framework/remote_session/os/os_session.py |=C2=A0 16 +-
=C2=A0.../remote_session/os/posix_session.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 |= =C2=A0 80 ++++++++
=C2=A0dts/framework/testbed_model/hw/__init__.py=C2=A0 =C2=A0 |=C2=A0 17 ++=
=C2=A0dts/framework/testbed_model/hw/cpu.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0| 164 ++++++++++++++++
=C2=A0dts/framework/testbed_model/node/node.py=C2=A0 =C2=A0 =C2=A0 |=C2=A0 = 36 ++++
=C2=A0dts/framework/testbed_model/node/sut_node.py=C2=A0 | 178 ++++++++++++= +++++-
=C2=A0dts/framework/utils.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 20 ++
=C2=A011 files changed, 634 insertions(+), 3 deletions(-)
=C2=A0create mode 100644 dts/framework/testbed_model/hw/__init__.py
=C2=A0create mode 100644 dts/framework/testbed_model/hw/cpu.py

diff --git a/dts/conf.yaml b/dts/conf.yaml
index 6b0bc5c2bf..976888a88e 100644
--- a/dts/conf.yaml
+++ b/dts/conf.yaml
@@ -12,4 +12,8 @@ nodes:
=C2=A0 =C2=A0- name: "SUT 1"
=C2=A0 =C2=A0 =C2=A0hostname: sut1.change.me.localhost
=C2=A0 =C2=A0 =C2=A0user: root
+=C2=A0 =C2=A0 arch: x86_64
=C2=A0 =C2=A0 =C2=A0os: linux
+=C2=A0 =C2=A0 bypass_core0: true
+=C2=A0 =C2=A0 cpus: ""
+=C2=A0 =C2=A0 memory_channels: 4
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init= __.py
index 1b97dc3ab9..344d697a69 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -11,12 +11,13 @@
=C2=A0import pathlib
=C2=A0from dataclasses import dataclass
=C2=A0from enum import Enum, auto, unique
-from typing import Any
+from typing import Any, Iterable

=C2=A0import warlock=C2=A0 # type: ignore
=C2=A0import yaml

=C2=A0from framework.settings import SETTINGS
+from framework.utils import expand_range


=C2=A0class StrEnum(Enum):
@@ -60,6 +61,80 @@ class Compiler(StrEnum):
=C2=A0 =C2=A0 =C2=A0msvc =3D auto()


+@dataclass(slots=3DTrue, frozen=3DTrue)
+class CPU:
+=C2=A0 =C2=A0 cpu: int
+=C2=A0 =C2=A0 core: int
+=C2=A0 =C2=A0 socket: int
+=C2=A0 =C2=A0 node: int
+
+=C2=A0 =C2=A0 def __str__(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return str(self.cpu)
+
+
+class CPUList(object):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Convert these options into a list of int cpus
+=C2=A0 =C2=A0 cpu_list=3D[CPU1, CPU2] - a list of CPUs
+=C2=A0 =C2=A0 cpu_list=3D[0,1,2,3] - a list of int indices
+=C2=A0 =C2=A0 cpu_list=3D['0','1','2-3'] - a list = of str indices; ranges are supported
+=C2=A0 =C2=A0 cpu_list=3D'0,1,2-3' - a comma delimited str of indi= ces; ranges are supported
+
+=C2=A0 =C2=A0 The class creates a unified format used across the framework= and allows
+=C2=A0 =C2=A0 the user to use either a str representation (using str(insta= nce) or directly
+=C2=A0 =C2=A0 in f-strings) or a list representation (by accessing instanc= e.cpu_list).
+=C2=A0 =C2=A0 Empty cpu_list is allowed.
+=C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 _cpu_list: list[int]
+
+=C2=A0 =C2=A0 def __init__(self, cpu_list: list[int | str | CPU] | str): +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._cpu_list =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if isinstance(cpu_list, str):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._from_str(cpu_list.split(&q= uot;,"))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._from_str((str(cpu) for cpu= in cpu_list))
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # the input cpus may not be sorted
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._cpu_list.sort()
+
+=C2=A0 =C2=A0 @property
+=C2=A0 =C2=A0 def cpu_list(self) -> list[int]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._cpu_list
+
+=C2=A0 =C2=A0 def _from_str(self, cpu_list: Iterable[str]) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu in cpu_list:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._cpu_list.extend(expand_ran= ge(cpu))
+
+=C2=A0 =C2=A0 def _get_consecutive_cpus_range(self, cpu_list: list[int]) -= > list[str]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted_core_list =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 tmp_cpus_list =3D list(sorted(cpu_list))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 segment =3D tmp_cpus_list[:1]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for core_id in tmp_cpus_list[1:]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if core_id - segment[-1] =3D=3D = 1:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 segment.append(cor= e_id)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted_core_lis= t.append(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;{segment[0]}-{segment[-1]}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if l= en(segment) > 1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else= f"{segment[0]}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 current_core_index= =3D tmp_cpus_list.index(core_id)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted_core_lis= t.extend(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= ._get_consecutive_cpus_range(tmp_cpus_list[current_core_index:])
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 segment.clear() +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(segment) > 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 formatted_core_list.append(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{segment[0]= }-{segment[-1]}" if len(segment) > 1 else f"{segment[0]}"=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return formatted_core_list
+
+=C2=A0 =C2=A0 def __str__(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f'{",".join(self._get_con= secutive_cpus_range(self._cpu_list))}'
+
+
=C2=A0# Slots enables some optimizations, by pre-allocating space for the d= efined
=C2=A0# attributes in the underlying data structure.
=C2=A0#
@@ -71,7 +146,11 @@ class NodeConfiguration:
=C2=A0 =C2=A0 =C2=A0hostname: str
=C2=A0 =C2=A0 =C2=A0user: str
=C2=A0 =C2=A0 =C2=A0password: str | None
+=C2=A0 =C2=A0 arch: Architecture
=C2=A0 =C2=A0 =C2=A0os: OS
+=C2=A0 =C2=A0 bypass_core0: bool
+=C2=A0 =C2=A0 cpus: CPUList
+=C2=A0 =C2=A0 memory_channels: int

=C2=A0 =C2=A0 =C2=A0@staticmethod
=C2=A0 =C2=A0 =C2=A0def from_dict(d: dict) -> "NodeConfiguration&qu= ot;:
@@ -80,7 +159,11 @@ def from_dict(d: dict) -> "NodeConfiguration&qu= ot;:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hostname=3Dd["hostname= "],
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0user=3Dd["user"],=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0password=3Dd.get("pass= word"),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 arch=3DArchitecture(d["arch= "]),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0os=3DOS(d["os"]),=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 bypass_core0=3Dd.get("bypas= s_core0", False),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 cpus=3DCPUList(d.get("cpus&= quot;, "1")),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 memory_channels=3Dd.get("me= mory_channels", 1),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)


diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framework/con= fig/conf_yaml_schema.json
index 409ce7ac74..c59d3e30e6 100644
--- a/dts/framework/config/conf_yaml_schema.json
+++ b/dts/framework/config/conf_yaml_schema.json
@@ -6,6 +6,12 @@
=C2=A0 =C2=A0 =C2=A0 =C2=A0"type": "string",
=C2=A0 =C2=A0 =C2=A0 =C2=A0"description": "A unique identifi= er for a node"
=C2=A0 =C2=A0 =C2=A0},
+=C2=A0 =C2=A0 "ARCH": {
+=C2=A0 =C2=A0 =C2=A0 "type": "string",
+=C2=A0 =C2=A0 =C2=A0 "enum": [
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 "x86_64"

arm64 and ppc64le should probably be included here. I think that w= e can focus on 64 bit arches for now.=C2=A0
=C2=A0
+=C2=A0 =C2=A0 =C2=A0 ]
+=C2=A0 =C2=A0 },
=C2=A0 =C2=A0 =C2=A0"OS": {
=C2=A0 =C2=A0 =C2=A0 =C2=A0"type": "string",
=C2=A0 =C2=A0 =C2=A0 =C2=A0"enum": [
@@ -82,8 +88,23 @@
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"type": "str= ing",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"description": &q= uot;The password to use on this node. Use only as a last resort. SSH keys a= re STRONGLY preferred."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0},
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "arch": {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": "#/defini= tions/ARCH"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"os": {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"$ref": "#/d= efinitions/OS"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "bypass_core0": {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "boolean&= quot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "I= ndicate that DPDK should omit using the first core."
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"cpus": {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string&q= uot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "O= ptional comma-separated list of cpus to use, e.g.: 1,2,3,4,5,18-22. Default= s to 1. An empty string means use all cpus."
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"memory_channels": {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "integer&= quot;,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "H= ow many memory channels to use. Optional, defaults to 1."
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0}
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0},
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"additionalProperties": false,<= br> @@ -91,6 +112,7 @@
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"name",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"hostname",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"user",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "arch",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"os"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0]
=C2=A0 =C2=A0 =C2=A0 =C2=A0},
diff --git a/dts/framework/remote_session/os/linux_session.py b/dts/framewo= rk/remote_session/os/linux_session.py
index 39e80631dd..21f117b714 100644
--- a/dts/framework/remote_session/os/linux_session.py
+++ b/dts/framework/remote_session/os/linux_session.py
@@ -2,6 +2,8 @@
=C2=A0# Copyright(c) 2022 PANTHEON.tech s.r.o.
=C2=A0# Copyright(c) 2022 University of New Hampshire

+from framework.config import CPU
+
=C2=A0from .posix_session import PosixSession


@@ -9,3 +11,16 @@ class LinuxSession(PosixSession):
=C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0The implementation of non-Posix compliant parts of Linu= x remote sessions.
=C2=A0 =C2=A0 =C2=A0"""
+
+=C2=A0 =C2=A0 def get_remote_cpus(self, bypass_core0: bool) -> list[CPU= ]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.remote_session.send_command(=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "lscpu -p=3DCPU,CORE,SOCKET= ,NODE|grep -v \\#"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpus =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu_line in cpu_info.splitlines():
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu, core, socket, node =3D cpu_= line.split(",")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if bypass_core0 and core =3D=3D = 0 and socket =3D=3D 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.logger.info= ("Core0 bypassed.")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 continue
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 cpus.append(CPU(int(cpu), int(co= re), int(socket), int(node)))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return cpus
diff --git a/dts/framework/remote_session/os/os_session.py b/dts/framework/= remote_session/os/os_session.py
index 57e2865282..6f6b6a979e 100644
--- a/dts/framework/remote_session/os/os_session.py
+++ b/dts/framework/remote_session/os/os_session.py
@@ -3,9 +3,10 @@
=C2=A0# Copyright(c) 2022 University of New Hampshire

=C2=A0from abc import ABC, abstractmethod
+from collections.abc import Iterable
=C2=A0from pathlib import PurePath

-from framework.config import Architecture, NodeConfiguration
+from framework.config import CPU, Architecture, NodeConfiguration
=C2=A0from framework.logger import DTSLOG
=C2=A0from framework.remote_session.factory import create_remote_session =C2=A0from framework.remote_session.remote_session import RemoteSession
@@ -130,3 +131,16 @@ def get_dpdk_version(self, version_path: str | PurePat= h) -> str:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Inspect DPDK version on the remote node f= rom version_path.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def get_remote_cpus(self, bypass_core0: bool) -> list[CPU= ]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Compose a list of CPUs present on the remote n= ode.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[= str]) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Kill and cleanup all DPDK apps identified by d= pdk_prefix_list. If
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_prefix_list is empty, attempt to find run= ning DPDK apps to kill and clean.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
diff --git a/dts/framework/remote_session/os/posix_session.py b/dts/framewo= rk/remote_session/os/posix_session.py
index a36b8e8c1a..7151263c7a 100644
--- a/dts/framework/remote_session/os/posix_session.py
+++ b/dts/framework/remote_session/os/posix_session.py
@@ -2,6 +2,8 @@
=C2=A0# Copyright(c) 2022 PANTHEON.tech s.r.o.
=C2=A0# Copyright(c) 2022 University of New Hampshire

+import re
+from collections.abc import Iterable
=C2=A0from pathlib import PurePath, PurePosixPath

=C2=A0from framework.config import Architecture
@@ -138,3 +140,81 @@ def get_dpdk_version(self, build_dir: str | PurePath) = -> str:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"cat {self.join_remot= e_path(build_dir, 'VERSION')}", verify=3DTrue
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return out.stdout
+
+=C2=A0 =C2=A0 def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[= str]) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.logger.info("Cleaning up DPDK apps= .")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_runtime_dirs =3D self._get_dpdk_runtime_d= irs(dpdk_prefix_list)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if dpdk_runtime_dirs:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # kill and cleanup only if DPDK = is running
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_pids =3D self._get_dpdk_pid= s(dpdk_runtime_dirs)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_pid in dpdk_pids:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_sessio= n.send_command(f"kill -9 {dpdk_pid}", 20)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._check_dpdk_hugepages(dpdk_= runtime_dirs)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remove_dpdk_runtime_dirs(d= pdk_runtime_dirs)
+
+=C2=A0 =C2=A0 def _get_dpdk_runtime_dirs(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, dpdk_prefix_list: Iterable[str]
+=C2=A0 =C2=A0 ) -> list[PurePosixPath]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix =3D PurePosixPath("/var", &qu= ot;run", "dpdk")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not dpdk_prefix_list:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_prefixes =3D self._list_r= emote_dirs(prefix)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if not remote_prefixes:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_prefix_list = =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_prefix_list = =3D remote_prefixes
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return [PurePosixPath(prefix, dpdk_prefix) for= dpdk_prefix in dpdk_prefix_list]
+
+=C2=A0 =C2=A0 def _list_remote_dirs(self, remote_path: str | PurePath) -&g= t; list[str] | None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Return a list of directories of the remote_dir= .
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 If remote_path doesn't exist, return None.=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"ls -l {remote_path} | awk= '/^d/ {{print $NF}}'"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if "No such file or directory" in ou= t:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return out.splitlines()
+
+=C2=A0 =C2=A0 def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[str | P= urePath]) -> list[int]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 pids =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 pid_regex =3D r"p(\d+)"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_runtime_dirs:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_config_file =3D PurePosixPa= th(dpdk_runtime_dir, "config")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_files_exists(dpd= k_config_file):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remot= e_session.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;lsof -Fp {dpdk_config_file}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if out and "N= o such file or directory" not in out:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for = out_line in out.splitlines():
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 match =3D re.match(pid_regex, out_line)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 if match:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 pids.append(int(match.group(1)))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return pids
+
+=C2=A0 =C2=A0 def _remote_files_exists(self, remote_path: PurePath) -> = bool:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(f&= quot;test -e {remote_path}")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return not result.return_code
+
+=C2=A0 =C2=A0 def _check_dpdk_hugepages(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, dpdk_runtime_dirs: Iterable[str | PurePa= th]
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_runtime_dirs:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_info =3D PurePosixPath(= dpdk_runtime_dir, "hugepage_info")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_files_exists(hug= epage_info):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remot= e_session.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;lsof -Fp {hugepage_info}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if out and "N= o such file or directory" not in out:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .logger.warning("Some DPDK processes did not free hugepages.") +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .logger.warning("*******************************************") +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .logger.warning(out)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .logger.warning("*******************************************") +
+=C2=A0 =C2=A0 def _remove_dpdk_runtime_dirs(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, dpdk_runtime_dirs: Iterable[str | PurePa= th]
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_runtime_dirs:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remove_remote_dir(dpdk_runt= ime_dir)
diff --git a/dts/framework/testbed_model/hw/__init__.py b/dts/framework/tes= tbed_model/hw/__init__.py
new file mode 100644
index 0000000000..7d79a7efd0
--- /dev/null
+++ b/dts/framework/testbed_model/hw/__init__.py
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2022 PANTHEON.tech s.r.o.
+
+from framework.config import CPU, CPUList
+
+from .cpu import CPUAmount, CPUAmountFilter, CPUFilter, CPUListFilter
+
+
+def cpu_filter(
+=C2=A0 =C2=A0 core_list: list[CPU], filter_specifier: CPUAmount | CPUList,= ascending: bool
+) -> CPUFilter:
+=C2=A0 =C2=A0 if isinstance(filter_specifier, CPUList):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CPUListFilter(core_list, filter_specifi= er, ascending)
+=C2=A0 =C2=A0 elif isinstance(filter_specifier, CPUAmount):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CPUAmountFilter(core_list, filter_speci= fier, ascending)
+=C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueError(f"Unsupported filter r{f= ilter_specifier}")
diff --git a/dts/framework/testbed_model/hw/cpu.py b/dts/framework/testbed_= model/hw/cpu.py
new file mode 100644
index 0000000000..87e87bcb4e
--- /dev/null
+++ b/dts/framework/testbed_model/hw/cpu.py
@@ -0,0 +1,164 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2022 PANTHEON.tech s.r.o.
+
+import dataclasses
+from abc import ABC, abstractmethod
+from collections.abc import Iterable
+
+from framework.config import CPU, CPUList
+
+
+@dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)
+class CPUAmount:
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Define the amounts of cpus to use. If sockets is not None, s= ocket_amount
+=C2=A0 =C2=A0 is ignored.
+=C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 cpus_per_core: int =3D 1
+=C2=A0 =C2=A0 cores_per_socket: int =3D 2
+=C2=A0 =C2=A0 socket_amount: int =3D 1
+=C2=A0 =C2=A0 sockets: list[int] | None =3D None
+
+
+class CPUFilter(ABC):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Filter according to the input filter specifier. Each filter = needs to be
+=C2=A0 =C2=A0 implemented in a derived class.
+=C2=A0 =C2=A0 This class only implements operations common to all filters,= such as sorting
+=C2=A0 =C2=A0 the list to be filtered beforehand.
+=C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 _filter_specifier: CPUAmount | CPUList
+=C2=A0 =C2=A0 _cpus_to_filter: list[CPU]
+
+=C2=A0 =C2=A0 def __init__(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 core_list: list[CPU],
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filter_specifier: CPUAmount | CPUList,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending: bool =3D True,
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._filter_specifier =3D filter_specifier +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # sorting by core is needed in case hyperthrea= ding is enabled
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._cpus_to_filter =3D sorted(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 core_list, key=3Dlambda x: x.cor= e, reverse=3Dnot ascending
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.filter()
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def filter(self) -> list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Use the input self._filter_specifier to filter= self._cpus_to_filter
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 and return the list of filtered CPUs. self._cp= us_to_filter is a
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sorter copy of the original list, so it may be= modified.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+
+
+class CPUAmountFilter(CPUFilter):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Filter the input list of CPUs according to specified rules:<= br> +=C2=A0 =C2=A0 Use cores from the specified amount of sockets or from the s= pecified socket ids.
+=C2=A0 =C2=A0 If sockets is specified, it takes precedence over socket_amo= unt.
+=C2=A0 =C2=A0 From each of those sockets, use only cores_per_socket of cor= es.
+=C2=A0 =C2=A0 And for each core, use cpus_per_core of cpus. Hypertheading<= br> +=C2=A0 =C2=A0 must be enabled for this to take effect.
+=C2=A0 =C2=A0 If ascending is True, use cores with the lowest numerical id= first
+=C2=A0 =C2=A0 and continue in ascending order. If False, start with the hi= ghest
+=C2=A0 =C2=A0 id and continue in descending order. This ordering affects w= hich
+=C2=A0 =C2=A0 sockets to consider first as well.
+=C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 _filter_specifier: CPUAmount
+
+=C2=A0 =C2=A0 def filter(self) -> list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._filter_cpus(self._filter_sockets(= self._cpus_to_filter))
+
+=C2=A0 =C2=A0 def _filter_sockets(self, cpus_to_filter: Iterable[CPU]) -&g= t; list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 allowed_sockets: set[int] =3D set()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 socket_amount =3D self._filter_specifier.socke= t_amount
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._filter_specifier.sockets:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 socket_amount =3D len(self._filt= er_specifier.sockets)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 allowed_sockets =3D set(self._fi= lter_specifier.sockets)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu in cpus_to_filter:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self._filter_specifier.so= ckets:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(allowed_soc= kets) < socket_amount:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 allo= wed_sockets.add(cpu.socket)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if cpu.socket in allowed_sockets= :
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus.appe= nd(cpu)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(allowed_sockets) < socket_amount: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueError(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"The amount = of sockets from which to use cores "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"({socket_am= ount}) exceeds the actual amount present "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"on the node= ({len(allowed_sockets)})"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return filtered_cpus
+
+=C2=A0 =C2=A0 def _filter_cpus(self, cpus_to_filter: Iterable[CPU]) -> = list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # no need to use ordered dict, from Python3.7 = the dict
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # insertion order is preserved (LIFO).
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 allowed_cpu_per_core_count_map: dict[int, int]= =3D {}
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu in cpus_to_filter:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if cpu.core in allowed_cpu_per_c= ore_count_map:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_count =3D allo= wed_cpu_per_core_count_map[cpu.core]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._filter_sp= ecifier.cpus_per_core > cpu_count:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # on= ly add cpus of the given core
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 allo= wed_cpu_per_core_count_map[cpu.core] +=3D 1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 filt= ered_cpus.append(cpu)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 rais= e ValueError(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"The amount of CPUs per core to use "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"({self._filter_specifier.cpus_per_core}) "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"exceeds the actual amount present. Is hyperthreading enab= led?"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ) +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 elif self._filter_specifier.core= s_per_socket > len(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 allowed_cpu_per_co= re_count_map
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # only add cpus if= we need more
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 allowed_cpu_per_co= re_count_map[cpu.core] =3D 1
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus.appe= nd(cpu)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # cpus are sorted = by core, at this point we won't encounter new cores
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 cores_per_socket =3D len(allowed_cpu_per_core_= count_map)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if cores_per_socket < self._filter_specifie= r.cores_per_socket:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueError(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"The amount = of cores per socket to use "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"({self._fil= ter_specifier.cores_per_socket}) "
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"exceeds the= actual amount present ({cores_per_socket})"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return filtered_cpus
+
+
+class CPUListFilter(CPUFilter):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Filter the input list of CPUs according to the input list of=
+=C2=A0 =C2=A0 core indices.
+=C2=A0 =C2=A0 An empty CPUList won't filter anything.
+=C2=A0 =C2=A0 """
+
+=C2=A0 =C2=A0 _filter_specifier: CPUList
+
+=C2=A0 =C2=A0 def filter(self) -> list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not len(self._filter_specifier.cpu_list): +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._cpus_to_filter
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus =3D []
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for core in self._cpus_to_filter:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if core.cpu in self._filter_spec= ifier.cpu_list:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus.appe= nd(core)
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(filtered_cpus) !=3D len(self._filter_sp= ecifier.cpu_list):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise ValueError(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Not all cpu= s from {self._filter_specifier.cpu_list} were found"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"among {self= ._cpus_to_filter}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return filtered_cpus
diff --git a/dts/framework/testbed_model/node/node.py b/dts/framework/testb= ed_model/node/node.py
index 86654e55ae..5ee7023335 100644
--- a/dts/framework/testbed_model/node/node.py
+++ b/dts/framework/testbed_model/node/node.py
@@ -8,13 +8,16 @@
=C2=A0"""

=C2=A0from framework.config import (
+=C2=A0 =C2=A0 CPU,
=C2=A0 =C2=A0 =C2=A0BuildTargetConfiguration,
+=C2=A0 =C2=A0 CPUList,
=C2=A0 =C2=A0 =C2=A0ExecutionConfiguration,
=C2=A0 =C2=A0 =C2=A0NodeConfiguration,
=C2=A0)
=C2=A0from framework.exception import NodeCleanupError, NodeSetupError, con= vert_exception
=C2=A0from framework.logger import DTSLOG, getLogger
=C2=A0from framework.remote_session import OSSession, create_session
+from framework.testbed_model.hw import CPUAmount, cpu_filter


=C2=A0class Node(object):
@@ -28,6 +31,7 @@ class Node(object):
=C2=A0 =C2=A0 =C2=A0main_session: OSSession
=C2=A0 =C2=A0 =C2=A0logger: DTSLOG
=C2=A0 =C2=A0 =C2=A0config: NodeConfiguration
+=C2=A0 =C2=A0 cpus: list[CPU]
=C2=A0 =C2=A0 =C2=A0_other_sessions: list[OSSession]

=C2=A0 =C2=A0 =C2=A0def __init__(self, node_config: NodeConfiguration):
@@ -38,6 +42,7 @@ def __init__(self, node_config: NodeConfiguration):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.logger =3D getLogger(self.name)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.logger.info(f"Created node:= {self.na= me}")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session =3D create_session(self= .config, = self.name, self.logger)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._get_remote_cpus()

=C2=A0 =C2=A0 =C2=A0@convert_exception(NodeSetupError)
=C2=A0 =C2=A0 =C2=A0def setup_execution(self, execution_config: ExecutionCo= nfiguration) -> None:
@@ -109,6 +114,37 @@ def create_session(self, name: str) -> OSSession: =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._other_sessions.append(connection) =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return connection

+=C2=A0 =C2=A0 def filter_cpus(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filter_specifier: CPUAmount | CPUList,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending: bool =3D True,
+=C2=A0 =C2=A0 ) -> list[CPU]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Filter the logical cpus found on the Node acco= rding to specified rules:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Use cores from the specified amount of sockets= or from the specified
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 socket ids. If sockets is specified, it takes = precedence over socket_amount.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 From each of those sockets, use only cpus_per_= socket of cores.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 And for each core, use cpus_per_core of cpus. = Hypertheading
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 must be enabled for this to take effect.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 If ascending is True, use cores with the lowes= t numerical id first
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 and continue in ascending order. If False, sta= rt with the highest
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 id and continue in descending order. This orde= ring affects which
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sockets to consider first as well.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.logger.info("Filtering ")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return cpu_filter(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.cpus,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 filter_specifier,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).filter()
+
+=C2=A0 =C2=A0 def _get_remote_cpus(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Scan cpus in the remote OS and store a list of= CPUs.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.logger.info("Getting CPU informati= on.")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.cpus =3D self.main_session.get_remote_cpu= s(self.config.bypass_core0)
+
=C2=A0 =C2=A0 =C2=A0def close(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Close all connections and free other reso= urces.
diff --git a/dts/framework/testbed_model/node/sut_node.py b/dts/framework/t= estbed_model/node/sut_node.py
index 53268a7565..ff3be845b4 100644
--- a/dts/framework/testbed_model/node/sut_node.py
+++ b/dts/framework/testbed_model/node/sut_node.py
@@ -4,10 +4,13 @@

=C2=A0import os
=C2=A0import tarfile
+import time
=C2=A0from pathlib import PurePath

-from framework.config import BuildTargetConfiguration, NodeConfiguration +from framework.config import CPU, BuildTargetConfiguration, CPUList, NodeC= onfiguration
+from framework.remote_session import OSSession
=C2=A0from framework.settings import SETTINGS
+from framework.testbed_model.hw import CPUAmount, CPUListFilter
=C2=A0from framework.utils import EnvVarsDict, skip_setup

=C2=A0from .node import Node
@@ -21,19 +24,31 @@ class SutNode(Node):
=C2=A0 =C2=A0 =C2=A0Another key capability is building DPDK according to gi= ven build target.
=C2=A0 =C2=A0 =C2=A0"""

+=C2=A0 =C2=A0 cpus: list[CPU]
+=C2=A0 =C2=A0 dpdk_prefix_list: list[str]
+=C2=A0 =C2=A0 dpdk_prefix_subfix: str
=C2=A0 =C2=A0 =C2=A0_build_target_config: BuildTargetConfiguration | None =C2=A0 =C2=A0 =C2=A0_env_vars: EnvVarsDict
=C2=A0 =C2=A0 =C2=A0_remote_tmp_dir: PurePath
=C2=A0 =C2=A0 =C2=A0__remote_dpdk_dir: PurePath | None
=C2=A0 =C2=A0 =C2=A0_app_compile_timeout: float
+=C2=A0 =C2=A0 _dpdk_kill_session: OSSession | None

=C2=A0 =C2=A0 =C2=A0def __init__(self, node_config: NodeConfiguration):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(SutNode, self).__init__(node_config= )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_prefix_list =3D []
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D None
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars =3D EnvVarsDict()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remote_tmp_dir =3D self.main_sessio= n.get_remote_tmp_dir()
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.__remote_dpdk_dir =3D None
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._app_compile_timeout =3D 90
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_kill_session =3D None
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # filter the node cpus according to user confi= g
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.cpus =3D CPUListFilter(self.cpus, self.co= nfig.cpus).filter()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_prefix_subfix =3D (
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{str(os.getpid())}_{time.= strftime('%Y%m%d%H%M%S', time.localtime())}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )

=C2=A0 =C2=A0 =C2=A0@property
=C2=A0 =C2=A0 =C2=A0def _remote_dpdk_dir(self) -> PurePath:
@@ -142,3 +157,164 @@ def build_dpdk_app(self, app_name: str) -> PurePat= h:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.join_remote_path= (
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0build_dir, "examples&q= uot;, f"dpdk-{app_name}"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
+
+=C2=A0 =C2=A0 def kill_cleanup_dpdk_apps(self) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Kill all dpdk applications on the SUT. Cleanup= hugepages.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._dpdk_kill_session and self._dpdk_kill= _session.is_alive():
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # we can use the session if it e= xists and responds
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_kill_session.kill_cle= anup_dpdk_apps(self.dpdk_prefix_list)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # otherwise, we need to (re)crea= te it
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_kill_session =3D self= .create_session("dpdk_kill")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.dpdk_prefix_list =3D []
+
+=C2=A0 =C2=A0 def create_eal_parameters(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_prefix: bool =3D False,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 core_filter_specifier: CPUAmount | CPUList =3D= CPUAmount(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending_cores: bool =3D True,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: str =3D "",
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: bool =3D False,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: list[str] =3D None,

I would prefer to have vdevs be a list of objects, even if= for now that class just takes a string in its constructor. Later on we can= add subclasses for specific vdevs that might see heavy use, such as=C2=A0l= ibrte_net_pcap and crypto_openssl.=C2=A0
=C2=A0
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: str =3D "",
+=C2=A0 =C2=A0 ) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param fixed_prefix: use fixed file-prefix or = not, when it is true,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 the file-prefix will not be added a timestamp
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param core_filter_specifier: an amount of cpu= s/cores/sockets to use
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 or a list of cpu ids to use.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 The default will select one cpu for each of two cores
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 on one socket, in ascending order of core ids.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param ascending_cores: True, use cores with t= he lowest numerical id first
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 and continue in ascending order. If False, start with the
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 highest id and continue in descending order. This ordering
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 affects which sockets to consider first as well.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 prefix=3D'vf';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus eg: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 no_pci=3DTrue;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 vdevs=3D['net_ring0', 'net_ring1'];
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK eal = parameters, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 other_eal_param=3D'--single-file-segments';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :return: eal param string, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 '-c 0xf -a 000= 0:88:00.0 --file-prefix=3Ddpdk_1112_20190809143420';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if DPDK version < 20.11-rc4, eal_str eg: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 '-c 0xf -w 000= 0:88:00.0 --file-prefix=3Ddpdk_1112_20190809143420';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if vdevs is None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs =3D []
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 config =3D {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "core_filter_specifier"= ;: core_filter_specifier,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "ascending_cores": asc= ending_cores,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "prefix": prefix,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "no_pci": no_pci,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "vdevs": vdevs,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "other_eal_param": oth= er_eal_param,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_parameter_creator =3D _EalParameter(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node=3Dself, fixed_prefix=3D= fixed_prefix, **config
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 eal_str =3D eal_parameter_creator.make_eal_par= am()
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return eal_str
+
+
+class _EalParameter(object):
+=C2=A0 =C2=A0 def __init__(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 sut_node: SutNode,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_prefix: bool,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 core_filter_specifier: CPUAmount | CPUList, +=C2=A0 =C2=A0 =C2=A0 =C2=A0 ascending_cores: bool,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 prefix: str,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 no_pci: bool,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 vdevs: list[str],
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 other_eal_param: str,
+=C2=A0 =C2=A0 ):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Generate eal parameters character string;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param sut_node: SUT Node;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param fixed_prefix: use fixed file-prefix or = not, when it is true,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 he file-prefix will not be added a timestamp
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param core_filter_specifier: an amount of cpu= s/cores/sockets to use
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 or a list of cpu ids to use.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param ascending_cores: True, use cores with t= he lowest numerical id first
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 and continue in ascending order. If False, start with the
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 highest id and continue in descending order. This ordering
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 affects which sockets to consider first as well.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param prefix: set file prefix string, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 prefix=3D'vf';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param no_pci: switch of disable PCI bus eg: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 no_pci=3DTrue;
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param vdevs: virtual device list, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 vdevs=3D['net_ring0', 'net_ring1'];
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 :param other_eal_param: user defined DPDK eal = parameters, eg:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 other_eal_param=3D'--single-file-segments';
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.os =3D sut_node.config.os
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.fixed_prefix =3D fixed_prefix
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node =3D sut_node
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.core_filter_specifier =3D core_filter_spe= cifier
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.ascending_cores =3D ascending_cores
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.prefix =3D prefix
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.no_pci =3D no_pci
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.vdevs =3D vdevs
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.other_eal_param =3D other_eal_param
+
+=C2=A0 =C2=A0 def _make_lcores_param(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 filtered_cpus =3D self.sut_node.filter_cpus( +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.core_filter_specifier, self= .ascending_cores
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"-l {CPUList(filtered_cpus)}"= ;
+
+=C2=A0 =C2=A0 def _make_memory_channels(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 param_template =3D "-n {}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return param_template.format(self.sut_node.con= fig.memory_channels)
+
+=C2=A0 =C2=A0 def _make_no_pci_param(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.no_pci is True:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return "--no-pci"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ""
+
+=C2=A0 =C2=A0 def _make_prefix_param(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.prefix =3D=3D "":
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_file_prefix =3D f"dpd= k_{self.sut_node.dpdk_prefix_subfix}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_file_prefix =3D self.prefi= x
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.fixed_prefix:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_file_prefix = =3D (
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;{fixed_file_prefix}_{self.sut_node.dpdk_prefix_subfix}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 fixed_file_prefix =3D self._do_os_handle_with_= prefix_param(fixed_file_prefix)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return fixed_file_prefix
+
+=C2=A0 =C2=A0 def _make_vdevs_param(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(self.vdevs) =3D=3D 0:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return " ".join(f"= ;--vdev {vdev}" for vdev in self.vdevs)
+
+=C2=A0 =C2=A0 def _do_os_handle_with_prefix_param(self, file_prefix: str) = -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sut_node.dpdk_prefix_list.append(file_pre= fix)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"--file-prefix=3D{file_prefix}&qu= ot;
+
+=C2=A0 =C2=A0 def make_eal_param(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 _eal_str =3D " ".join(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._make_lcores_= param(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._make_memory_= channels(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._make_prefix_= param(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._make_no_pci_= param(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._make_vdevs_p= aram(),
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # append user defi= ned eal parameters
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.other_eal_par= am,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return _eal_str
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 91e58f3218..3c2f0adff9 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -32,6 +32,26 @@ def skip_setup(func) -> Callable[..., None]:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return func


+def expand_range(range_str: str) -> list[int]:
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Process range string into a list of integers. There are two = possible formats:
+=C2=A0 =C2=A0 n - a single integer
+=C2=A0 =C2=A0 n-m - a range of integers
+
+=C2=A0 =C2=A0 The returned range includes both n and m. Empty string retur= ns an empty list.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 expanded_range: list[int] =3D []
+=C2=A0 =C2=A0 if range_str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 range_boundaries =3D range_str.split("-&q= uot;)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # will throw an exception when items in range_= boundaries can't be converted,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 # serving as type check
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 expanded_range.extend(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 range(int(range_boundaries[0]), = int(range_boundaries[-1]) + 1)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
+
+=C2=A0 =C2=A0 return expanded_range
+
+
=C2=A0def GREEN(text: str) -> str:
=C2=A0 =C2=A0 =C2=A0return f"\u001B[32;1m{str(text)}\u001B[0m"
--
2.30.2

--000000000000e3f07c05ed967515--