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 ECC3E42D17; Wed, 21 Jun 2023 20:33:39 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id C47E14068E; Wed, 21 Jun 2023 20:33:39 +0200 (CEST) Received: from mail-pj1-f47.google.com (mail-pj1-f47.google.com [209.85.216.47]) by mails.dpdk.org (Postfix) with ESMTP id 59E664003C for ; Wed, 21 Jun 2023 20:33:37 +0200 (CEST) Received: by mail-pj1-f47.google.com with SMTP id 98e67ed59e1d1-25ee8d84b4fso3387761a91.0 for ; Wed, 21 Jun 2023 11:33:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1687372416; x=1689964416; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=9UUkPscWlojH941ssYKA5uml7b4Lrup/G2cCiQzc7/w=; b=LX4TajPCEYpU1b5lxT3WhkSLZgqcUdimY91s2rs02VGvQHWB1prS5t1cIoeU3V1FmG ez0/lsfAIc3px+7e4VlF5oSqYgRD/AptrfD9K/blwq147t+0y+0vCzj18p1NPHREWopW 2v844Gsv77E48kWCYG+Ji6Yw5DMrfbbblUF2Y= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1687372416; x=1689964416; 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=9UUkPscWlojH941ssYKA5uml7b4Lrup/G2cCiQzc7/w=; b=d7mIYaphIlVLamj2JVvo0M8YNKu0ngVvLBkzCrnZ7kYaDG3pDIAT2ifzKTr99E5doH 8jfggWHJFCMgZ+6wJXkzcnuLr076wsVyyGjgCIytFraUfgasmGhRwNK7mr6x881ODcSH OwSOPuvReGxrza7m7oLwwxyqv+5KxVQlxGVbzA+Hz3YYxZbs9k0lBYgpGHvCDm5lkxeQ bbx2nslXrs3R6rnTsgIQj/ID5ctYieNBj1Ouko9Zp7plbSgtuzoVGlqZaUhnwkJfbx1R A/2Ew6/8Hq4OwuZcGSKNbXPLVtYtvFdIlokHZDant6a3G49S2ndVg6/33SqeNhxFxuQC KJrw== X-Gm-Message-State: AC+VfDyhFrfLMuCmvYB3M7Jpq42doSN3dbC0YWeEmnUgNpC5Kp00dxfc FG5igG8IeeEWBQHaylYmzoo87UNgkPIDltWjKJaRCg== X-Google-Smtp-Source: ACHHUZ5Gjt5ZRwhJrpolx1ET2SpJyVSbNB3gj3c16ACiJVvNYBeqgevNjks+Fyt8tlpF0aSeiCGS7FVWzzayfrzkWx4= X-Received: by 2002:a17:90a:31c:b0:260:e7ad:3659 with SMTP id 28-20020a17090a031c00b00260e7ad3659mr3320546pje.12.1687372416257; Wed, 21 Jun 2023 11:33:36 -0700 (PDT) MIME-Version: 1.0 References: <20230424133537.58698-1-juraj.linkes@pantheon.tech> <20230609094640.130843-1-juraj.linkes@pantheon.tech> In-Reply-To: <20230609094640.130843-1-juraj.linkes@pantheon.tech> From: Jeremy Spewock Date: Wed, 21 Jun 2023 14:33:25 -0400 Message-ID: Subject: Re: [PATCH v3] dts: replace pexpect with fabric To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, probb@iol.unh.edu, dev@dpdk.org Content-Type: multipart/alternative; boundary="00000000000079a8d305fea80193" 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 --00000000000079a8d305fea80193 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Acked-by: Jeremy Spewock On Fri, Jun 9, 2023 at 5:46=E2=80=AFAM Juraj Linke=C5=A1 wrote: > Pexpect is not a dedicated SSH connection library while Fabric is. With > Fabric, all SSH-related logic is provided and we can just focus on > what's DTS specific. > > Signed-off-by: Juraj Linke=C5=A1 > --- > > Notes: > v3: updated passwordless sudo setup on Linux > > doc/guides/tools/dts.rst | 29 +- > dts/conf.yaml | 2 +- > dts/framework/exception.py | 10 +- > dts/framework/remote_session/linux_session.py | 31 +- > dts/framework/remote_session/os_session.py | 51 +++- > dts/framework/remote_session/posix_session.py | 48 +-- > .../remote_session/remote/remote_session.py | 35 ++- > .../remote_session/remote/ssh_session.py | 287 ++++++------------ > dts/framework/testbed_model/sut_node.py | 12 +- > dts/framework/utils.py | 9 - > dts/poetry.lock | 161 ++++++++-- > dts/pyproject.toml | 2 +- > 12 files changed, 376 insertions(+), 301 deletions(-) > > diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst > index ebd6dceb6a..c7b31623e4 100644 > --- a/doc/guides/tools/dts.rst > +++ b/doc/guides/tools/dts.rst > @@ -95,9 +95,14 @@ Setting up DTS environment > > #. **SSH Connection** > > - DTS uses Python pexpect for SSH connections between DTS environment > and the other hosts. > - The pexpect implementation is a wrapper around the ssh command in the > DTS environment. > - This means it'll use the SSH agent providing the ssh command and its > keys. > + DTS uses the Fabric Python library for SSH connections between DTS > environment > + and the other hosts. > + The authentication method used is pubkey authentication. > + Fabric tries to use a passed key/certificate, > + then any key it can with through an SSH agent, > + then any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in > ``~/.ssh/`` > + (with any matching OpenSSH-style certificates). > + DTS doesn't pass any keys, so Fabric tries to use the other two > methods. > > > Setting up System Under Test > @@ -132,6 +137,21 @@ There are two areas that need to be set up on a > System Under Test: > It's possible to use the hugepage configuration already present on > the SUT. > If you wish to do so, don't specify the hugepage configuration in > the DTS config file. > > +#. **User with administrator privileges** > + > +.. _sut_admin_user: > + > + DTS needs administrator privileges to run DPDK applications (such as > testpmd) on the SUT. > + The SUT user must be able run commands in privileged mode without > asking for password. > + On most Linux distributions, it's a matter of setting up passwordless > sudo: > + > + #. Run ``sudo visudo`` and check that it contains ``%sudo > ALL=3D(ALL:ALL) NOPASSWD:ALL``. > + > + #. Add the SUT user to the sudo group with: > + > + .. code-block:: console > + > + sudo usermod -aG sudo > > Running DTS > ----------- > @@ -151,7 +171,8 @@ which is a template that illustrates what can be > configured in DTS: > :start-at: executions: > > > -The user must be root or any other user with prompt starting with ``#``. > +The user must have :ref:`administrator privileges ` > +which don't require password authentication. > The other fields are mostly self-explanatory > and documented in more detail in > ``dts/framework/config/conf_yaml_schema.json``. > > diff --git a/dts/conf.yaml b/dts/conf.yaml > index a9bd8a3ecf..129801d87c 100644 > --- a/dts/conf.yaml > +++ b/dts/conf.yaml > @@ -16,7 +16,7 @@ executions: > nodes: > - name: "SUT 1" > hostname: sut1.change.me.localhost > - user: root > + user: dtsuser > arch: x86_64 > os: linux > lcores: "" > diff --git a/dts/framework/exception.py b/dts/framework/exception.py > index ca353d98fc..44ff4e979a 100644 > --- a/dts/framework/exception.py > +++ b/dts/framework/exception.py > @@ -62,13 +62,19 @@ class SSHConnectionError(DTSError): > """ > > host: str > + errors: list[str] > severity: ClassVar[ErrorSeverity] =3D ErrorSeverity.SSH_ERR > > - def __init__(self, host: str): > + def __init__(self, host: str, errors: list[str] | None =3D None): > self.host =3D host > + self.errors =3D [] if errors is None else errors > > def __str__(self) -> str: > - return f"Error trying to connect with {self.host}" > + message =3D f"Error trying to connect with {self.host}." > + if self.errors: > + message +=3D f" Errors encountered while retrying: {', > '.join(self.errors)}" > + > + return message > > > class SSHSessionDeadError(DTSError): > diff --git a/dts/framework/remote_session/linux_session.py > b/dts/framework/remote_session/linux_session.py > index a1e3bc3a92..f13f399121 100644 > --- a/dts/framework/remote_session/linux_session.py > +++ b/dts/framework/remote_session/linux_session.py > @@ -14,10 +14,11 @@ class LinuxSession(PosixSession): > The implementation of non-Posix compliant parts of Linux remote > sessions. > """ > > + def _get_privileged_command(self, command: str) -> str: > + return f"sudo -- sh -c '{command}'" > + > def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]= : > - cpu_info =3D self.remote_session.send_command( > - "lscpu -p=3DCPU,CORE,SOCKET,NODE|grep -v \\#" > - ).stdout > + cpu_info =3D self.send_command("lscpu -p=3DCPU,CORE,SOCKET,NODE|= grep > -v \\#").stdout > lcores =3D [] > for cpu_line in cpu_info.splitlines(): > lcore, core, socket, node =3D map(int, cpu_line.split(",")) > @@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int, > force_first_numa: bool) -> None: > self._mount_huge_pages() > > def _get_hugepage_size(self) -> int: > - hugepage_size =3D self.remote_session.send_command( > + hugepage_size =3D self.send_command( > "awk '/Hugepagesize/ {print $2}' /proc/meminfo" > ).stdout > return int(hugepage_size) > > def _get_hugepages_total(self) -> int: > - hugepages_total =3D self.remote_session.send_command( > + hugepages_total =3D self.send_command( > "awk '/HugePages_Total/ { print $2 }' /proc/meminfo" > ).stdout > return int(hugepages_total) > > def _get_numa_nodes(self) -> list[int]: > try: > - numa_count =3D self.remote_session.send_command( > + numa_count =3D self.send_command( > "cat /sys/devices/system/node/online", verify=3DTrue > ).stdout > numa_range =3D expand_range(numa_count) > @@ -70,14 +71,12 @@ def _get_numa_nodes(self) -> list[int]: > def _mount_huge_pages(self) -> None: > self._logger.info("Re-mounting Hugepages.") > hugapge_fs_cmd =3D "awk '/hugetlbfs/ { print $2 }' /proc/mounts" > - self.remote_session.send_command(f"umount $({hugapge_fs_cmd})") > - result =3D self.remote_session.send_command(hugapge_fs_cmd) > + self.send_command(f"umount $({hugapge_fs_cmd})") > + result =3D self.send_command(hugapge_fs_cmd) > if result.stdout =3D=3D "": > remote_mount_path =3D "/mnt/huge" > - self.remote_session.send_command(f"mkdir -p > {remote_mount_path}") > - self.remote_session.send_command( > - f"mount -t hugetlbfs nodev {remote_mount_path}" > - ) > + self.send_command(f"mkdir -p {remote_mount_path}") > + self.send_command(f"mount -t hugetlbfs nodev > {remote_mount_path}") > > def _supports_numa(self) -> bool: > # the system supports numa if self._numa_nodes is non-empty and > there are more > @@ -94,14 +93,12 @@ def _configure_huge_pages( > ) > if force_first_numa and self._supports_numa(): > # clear non-numa hugepages > - self.remote_session.send_command( > - f"echo 0 | sudo tee {hugepage_config_path}" > - ) > + self.send_command(f"echo 0 | tee {hugepage_config_path}", > privileged=3DTrue) > hugepage_config_path =3D ( > > f"/sys/devices/system/node/node{self._numa_nodes[0]}/hugepages" > f"/hugepages-{size}kB/nr_hugepages" > ) > > - self.remote_session.send_command( > - f"echo {amount} | sudo tee {hugepage_config_path}" > + self.send_command( > + f"echo {amount} | tee {hugepage_config_path}", privileged=3D= True > ) > diff --git a/dts/framework/remote_session/os_session.py > b/dts/framework/remote_session/os_session.py > index 4c48ae2567..bfd70bd480 100644 > --- a/dts/framework/remote_session/os_session.py > +++ b/dts/framework/remote_session/os_session.py > @@ -10,7 +10,7 @@ > from framework.logger import DTSLOG > from framework.settings import SETTINGS > from framework.testbed_model import LogicalCore > -from framework.utils import EnvVarsDict, MesonArgs > +from framework.utils import MesonArgs > > from .remote import CommandResult, RemoteSession, create_remote_session > > @@ -53,17 +53,32 @@ def is_alive(self) -> bool: > def send_command( > self, > command: str, > - timeout: float, > + timeout: float =3D SETTINGS.timeout, > + privileged: bool =3D False, > verify: bool =3D False, > - env: EnvVarsDict | None =3D None, > + env: dict | None =3D None, > ) -> CommandResult: > """ > An all-purpose API in case the command to be executed is already > OS-agnostic, such as when the path to the executed command has > been > constructed beforehand. > """ > + if privileged: > + command =3D self._get_privileged_command(command) > + > return self.remote_session.send_command(command, timeout, verify= , > env) > > + @abstractmethod > + def _get_privileged_command(self, command: str) -> str: > + """Modify the command so that it executes with administrative > privileges. > + > + Args: > + command: The command to modify. > + > + Returns: > + The modified command that executes with administrative > privileges. > + """ > + > @abstractmethod > def guess_dpdk_remote_dir(self, remote_dir) -> PurePath: > """ > @@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PurePath) -> > PurePath: > """ > > @abstractmethod > - def copy_file( > + def copy_from( > self, > source_file: str | PurePath, > destination_file: str | PurePath, > - source_remote: bool =3D False, > ) -> None: > + """Copy a file from the remote Node to the local filesystem. > + > + Copy source_file from the remote Node associated with this remot= e > + session to destination_file on the local filesystem. > + > + Args: > + source_file: the file on the remote Node. > + destination_file: a file or directory path on the local > filesystem. > """ > + > + @abstractmethod > + def copy_to( > + self, > + source_file: str | PurePath, > + destination_file: str | PurePath, > + ) -> None: > + """Copy a file from local filesystem to the remote Node. > + > Copy source_file from local filesystem to destination_file > - on the remote Node associated with the remote session. > - If source_remote is True, reverse the direction - copy > source_file from the > - associated remote Node to destination_file on local storage. > + on the remote Node associated with this remote session. > + > + Args: > + source_file: the file on the local filesystem. > + destination_file: a file or directory path on the remote Nod= e. > """ > > @abstractmethod > @@ -128,7 +161,7 @@ def extract_remote_tarball( > @abstractmethod > def build_dpdk( > self, > - env_vars: EnvVarsDict, > + env_vars: dict, > meson_args: MesonArgs, > remote_dpdk_dir: str | PurePath, > remote_dpdk_build_dir: str | PurePath, > diff --git a/dts/framework/remote_session/posix_session.py > b/dts/framework/remote_session/posix_session.py > index d38062e8d6..8ca0acb429 100644 > --- a/dts/framework/remote_session/posix_session.py > +++ b/dts/framework/remote_session/posix_session.py > @@ -9,7 +9,7 @@ > from framework.config import Architecture > from framework.exception import DPDKBuildError, > RemoteCommandExecutionError > from framework.settings import SETTINGS > -from framework.utils import EnvVarsDict, MesonArgs > +from framework.utils import MesonArgs > > from .os_session import OSSession > > @@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -> str: > > def guess_dpdk_remote_dir(self, remote_dir) -> PurePosixPath: > remote_guess =3D self.join_remote_path(remote_dir, "dpdk-*") > - result =3D self.remote_session.send_command(f"ls -d {remote_gues= s} > | tail -1") > + result =3D self.send_command(f"ls -d {remote_guess} | tail -1") > return PurePosixPath(result.stdout) > > def get_remote_tmp_dir(self) -> PurePosixPath: > @@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architecture) -= > > dict: > env_vars =3D {} > if arch =3D=3D Architecture.i686: > # find the pkg-config path and store it in PKG_CONFIG_LIBDIR > - out =3D self.remote_session.send_command("find /usr -type d > -name pkgconfig") > + out =3D self.send_command("find /usr -type d -name pkgconfig= ") > pkg_path =3D "" > res_path =3D out.stdout.split("\r\n") > for cur_path in res_path: > @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Architecture) > -> dict: > def join_remote_path(self, *args: str | PurePath) -> PurePosixPath: > return PurePosixPath(*args) > > - def copy_file( > + def copy_from( > self, > source_file: str | PurePath, > destination_file: str | PurePath, > - source_remote: bool =3D False, > ) -> None: > - self.remote_session.copy_file(source_file, destination_file, > source_remote) > + self.remote_session.copy_from(source_file, destination_file) > + > + def copy_to( > + self, > + source_file: str | PurePath, > + destination_file: str | PurePath, > + ) -> None: > + self.remote_session.copy_to(source_file, destination_file) > > def remove_remote_dir( > self, > @@ -80,24 +86,24 @@ def remove_remote_dir( > force: bool =3D True, > ) -> None: > opts =3D PosixSession.combine_short_options(r=3Drecursive, f=3Df= orce) > - self.remote_session.send_command(f"rm{opts} {remote_dir_path}") > + self.send_command(f"rm{opts} {remote_dir_path}") > > def extract_remote_tarball( > self, > remote_tarball_path: str | PurePath, > expected_dir: str | PurePath | None =3D None, > ) -> None: > - self.remote_session.send_command( > + self.send_command( > f"tar xfm {remote_tarball_path} " > f"-C {PurePosixPath(remote_tarball_path).parent}", > 60, > ) > if expected_dir: > - self.remote_session.send_command(f"ls {expected_dir}", > verify=3DTrue) > + self.send_command(f"ls {expected_dir}", verify=3DTrue) > > def build_dpdk( > self, > - env_vars: EnvVarsDict, > + env_vars: dict, > meson_args: MesonArgs, > remote_dpdk_dir: str | PurePath, > remote_dpdk_build_dir: str | PurePath, > @@ -108,7 +114,7 @@ def build_dpdk( > if rebuild: > # reconfigure, then build > self._logger.info("Reconfiguring DPDK build.") > - self.remote_session.send_command( > + self.send_command( > f"meson configure {meson_args} > {remote_dpdk_build_dir}", > timeout, > verify=3DTrue, > @@ -118,7 +124,7 @@ def build_dpdk( > # fresh build - remove target dir first, then build from > scratch > self._logger.info("Configuring DPDK build from scratch."= ) > self.remove_remote_dir(remote_dpdk_build_dir) > - self.remote_session.send_command( > + self.send_command( > f"meson setup " > f"{meson_args} {remote_dpdk_dir} > {remote_dpdk_build_dir}", > timeout, > @@ -127,14 +133,14 @@ def build_dpdk( > ) > > self._logger.info("Building DPDK.") > - self.remote_session.send_command( > + self.send_command( > f"ninja -C {remote_dpdk_build_dir}", timeout, > verify=3DTrue, env=3Denv_vars > ) > except RemoteCommandExecutionError as e: > raise DPDKBuildError(f"DPDK build failed when doing > '{e.command}'.") > > def get_dpdk_version(self, build_dir: str | PurePath) -> str: > - out =3D self.remote_session.send_command( > + out =3D self.send_command( > f"cat {self.join_remote_path(build_dir, 'VERSION')}", > verify=3DTrue > ) > return out.stdout > @@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: > Iterable[str]) -> None: > # 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.send_command(f"kill -9 {dpdk_pid}", 20) > self._check_dpdk_hugepages(dpdk_runtime_dirs) > self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs) > > @@ -168,7 +174,7 @@ 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( > + out =3D self.send_command( > f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'" > ).stdout > if "No such file or directory" in out: > @@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: > Iterable[str | PurePath]) -> list[in > 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 > + out =3D self.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) > @@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: > Iterable[str | PurePath]) -> list[in > return pids > > def _remote_files_exists(self, remote_path: PurePath) -> bool: > - result =3D self.remote_session.send_command(f"test -e > {remote_path}") > + result =3D self.send_command(f"test -e {remote_path}") > return not result.return_code > > def _check_dpdk_hugepages( > @@ -202,9 +206,7 @@ def _check_dpdk_hugepages( > 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 > + out =3D self.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 > free hugepages.") > > self._logger.warning("*******************************************") > diff --git a/dts/framework/remote_session/remote/remote_session.py > b/dts/framework/remote_session/remote/remote_session.py > index 91dee3cb4f..0647d93de4 100644 > --- a/dts/framework/remote_session/remote/remote_session.py > +++ b/dts/framework/remote_session/remote/remote_session.py > @@ -11,7 +11,6 @@ > from framework.exception import RemoteCommandExecutionError > from framework.logger import DTSLOG > from framework.settings import SETTINGS > -from framework.utils import EnvVarsDict > > > @dataclasses.dataclass(slots=3DTrue, frozen=3DTrue) > @@ -89,7 +88,7 @@ def send_command( > command: str, > timeout: float =3D SETTINGS.timeout, > verify: bool =3D False, > - env: EnvVarsDict | None =3D None, > + env: dict | None =3D None, > ) -> CommandResult: > """ > Send a command to the connected node using optional env vars > @@ -114,7 +113,7 @@ def send_command( > > @abstractmethod > def _send_command( > - self, command: str, timeout: float, env: EnvVarsDict | None > + self, command: str, timeout: float, env: dict | None > ) -> CommandResult: > """ > Use the underlying protocol to execute the command using optiona= l > env vars > @@ -141,15 +140,33 @@ def is_alive(self) -> bool: > """ > > @abstractmethod > - def copy_file( > + def copy_from( > self, > source_file: str | PurePath, > destination_file: str | PurePath, > - source_remote: bool =3D False, > ) -> None: > + """Copy a file from the remote Node to the local filesystem. > + > + Copy source_file from the remote Node associated with this remot= e > + session to destination_file on the local filesystem. > + > + Args: > + source_file: the file on the remote Node. > + destination_file: a file or directory path on the local > filesystem. > """ > - Copy source_file from local filesystem to destination_file on th= e > remote Node > - associated with the remote session. > - If source_remote is True, reverse the direction - copy > source_file from the > - associated Node to destination_file on local filesystem. > + > + @abstractmethod > + def copy_to( > + self, > + source_file: str | PurePath, > + destination_file: str | PurePath, > + ) -> None: > + """Copy a file from local filesystem to the remote Node. > + > + Copy source_file from local filesystem to destination_file > + on the remote Node associated with this remote session. > + > + Args: > + source_file: the file on the local filesystem. > + destination_file: a file or directory path on the remote Nod= e. > """ > diff --git a/dts/framework/remote_session/remote/ssh_session.py > b/dts/framework/remote_session/remote/ssh_session.py > index 42ff9498a2..8d127f1601 100644 > --- a/dts/framework/remote_session/remote/ssh_session.py > +++ b/dts/framework/remote_session/remote/ssh_session.py > @@ -1,29 +1,49 @@ > # SPDX-License-Identifier: BSD-3-Clause > -# Copyright(c) 2010-2014 Intel Corporation > -# Copyright(c) 2022-2023 PANTHEON.tech s.r.o. > -# Copyright(c) 2022-2023 University of New Hampshire > +# Copyright(c) 2023 PANTHEON.tech s.r.o. > > -import time > +import socket > +import traceback > from pathlib import PurePath > > -import pexpect # type: ignore > -from pexpect import pxssh # type: ignore > +from fabric import Connection # type: ignore[import] > +from invoke.exceptions import ( # type: ignore[import] > + CommandTimedOut, > + ThreadException, > + UnexpectedExit, > +) > +from paramiko.ssh_exception import ( # type: ignore[import] > + AuthenticationException, > + BadHostKeyException, > + NoValidConnectionsError, > + SSHException, > +) > > from framework.config import NodeConfiguration > from framework.exception import SSHConnectionError, SSHSessionDeadError, > SSHTimeoutError > from framework.logger import DTSLOG > -from framework.utils import GREEN, RED, EnvVarsDict > > from .remote_session import CommandResult, RemoteSession > > > class SSHSession(RemoteSession): > - """ > - Module for creating Pexpect SSH remote sessions. > + """A persistent SSH connection to a remote Node. > + > + The connection is implemented with the Fabric Python library. > + > + Args: > + node_config: The configuration of the Node to connect to. > + session_name: The name of the session. > + logger: The logger used for logging. > + This should be passed from the parent OSSession. > + > + Attributes: > + session: The underlying Fabric SSH connection. > + > + Raises: > + SSHConnectionError: The connection cannot be established. > """ > > - session: pxssh.pxssh > - magic_prompt: str > + session: Connection > > def __init__( > self, > @@ -31,218 +51,91 @@ def __init__( > session_name: str, > logger: DTSLOG, > ): > - self.magic_prompt =3D "MAGIC PROMPT" > super(SSHSession, self).__init__(node_config, session_name, > logger) > > def _connect(self) -> None: > - """ > - Create connection to assigned node. > - """ > + errors =3D [] > retry_attempts =3D 10 > login_timeout =3D 20 if self.port else 10 > - password_regex =3D ( > - r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password for > .+:)" > - ) > - try: > - for retry_attempt in range(retry_attempts): > - self.session =3D pxssh.pxssh(encoding=3D"utf-8") > - try: > - self.session.login( > - self.ip, > - self.username, > - self.password, > - original_prompt=3D"[$#>]", > - port=3Dself.port, > - login_timeout=3Dlogin_timeout, > - password_regex=3Dpassword_regex, > - ) > - break > - except Exception as e: > - self._logger.warning(e) > - time.sleep(2) > - self._logger.info( > - f"Retrying connection: retry number > {retry_attempt + 1}." > - ) > - else: > - raise Exception(f"Connection to {self.hostname} failed") > - > - self.send_expect("stty -echo", "#") > - self.send_expect("stty columns 1000", "#") > - self.send_expect("bind 'set enable-bracketed-paste off'", "#= ") > - except Exception as e: > - self._logger.error(RED(str(e))) > - if getattr(self, "port", None): > - suggestion =3D ( > - f"\nSuggestion: Check if the firewall on > {self.hostname} is " > - f"stopped.\n" > + for retry_attempt in range(retry_attempts): > + try: > + self.session =3D Connection( > + self.ip, > + user=3Dself.username, > + port=3Dself.port, > + connect_kwargs=3D{"password": self.password}, > + connect_timeout=3Dlogin_timeout, > ) > - self._logger.info(GREEN(suggestion)) > - > - raise SSHConnectionError(self.hostname) > + self.session.open() > > - def send_expect( > - self, command: str, prompt: str, timeout: float =3D 15, verify: > bool =3D False > - ) -> str | int: > - try: > - ret =3D self.send_expect_base(command, prompt, timeout) > - if verify: > - ret_status =3D self.send_expect_base("echo $?", prompt, > timeout) > - try: > - retval =3D int(ret_status) > - if retval: > - self._logger.error(f"Command: {command} failure!= ") > - self._logger.error(ret) > - return retval > - else: > - return ret > - except ValueError: > - return ret > - else: > - return ret > - except Exception as e: > - self._logger.error( > - f"Exception happened in [{command}] and output is " > - f"[{self._get_output()}]" > - ) > - raise e > - > - def send_expect_base(self, command: str, prompt: str, timeout: float= ) > -> str: > - self._clean_session() > - original_prompt =3D self.session.PROMPT > - self.session.PROMPT =3D prompt > - self._send_line(command) > - self._prompt(command, timeout) > - > - before =3D self._get_output() > - self.session.PROMPT =3D original_prompt > - return before > - > - def _clean_session(self) -> None: > - self.session.PROMPT =3D self.magic_prompt > - self.get_output(timeout=3D0.01) > - self.session.PROMPT =3D self.session.UNIQUE_PROMPT > - > - def _send_line(self, command: str) -> None: > - if not self.is_alive(): > - raise SSHSessionDeadError(self.hostname) > - if len(command) =3D=3D 2 and command.startswith("^"): > - self.session.sendcontrol(command[1]) > - else: > - self.session.sendline(command) > + except (ValueError, BadHostKeyException, > AuthenticationException) as e: > + self._logger.exception(e) > + raise SSHConnectionError(self.hostname) from e > > - def _prompt(self, command: str, timeout: float) -> None: > - if not self.session.prompt(timeout): > - raise SSHTimeoutError(command, self._get_output()) from None > + except (NoValidConnectionsError, socket.error, SSHException) > as e: > + self._logger.debug(traceback.format_exc()) > + self._logger.warning(e) > > - def get_output(self, timeout: float =3D 15) -> str: > - """ > - Get all output before timeout > - """ > - try: > - self.session.prompt(timeout) > - except Exception: > - pass > - > - before =3D self._get_output() > - self._flush() > - > - return before > + error =3D repr(e) > + if error not in errors: > + errors.append(error) > > - def _get_output(self) -> str: > - if not self.is_alive(): > - raise SSHSessionDeadError(self.hostname) > - before =3D self.session.before.rsplit("\r\n", 1)[0] > - if before =3D=3D "[PEXPECT]": > - return "" > - return before > + self._logger.info( > + f"Retrying connection: retry number {retry_attempt + > 1}." > + ) > > - def _flush(self) -> None: > - """ > - Clear all session buffer > - """ > - self.session.buffer =3D "" > - self.session.before =3D "" > + else: > + break > + else: > + raise SSHConnectionError(self.hostname, errors) > > def is_alive(self) -> bool: > - return self.session.isalive() > + return self.session.is_connected > > def _send_command( > - self, command: str, timeout: float, env: EnvVarsDict | None > + self, command: str, timeout: float, env: dict | None > ) -> CommandResult: > - output =3D self._send_command_get_output(command, timeout, env) > - return_code =3D int(self._send_command_get_output("echo $?", > timeout, None)) > + """Send a command and return the result of the execution. > > - # we're capturing only stdout > - return CommandResult(self.name, command, output, "", return_code= ) > + Args: > + command: The command to execute. > + timeout: Wait at most this many seconds for the execution to > complete. > + env: Extra environment variables that will be used in comman= d > execution. > > - def _send_command_get_output( > - self, command: str, timeout: float, env: EnvVarsDict | None > - ) -> str: > + Raises: > + SSHSessionDeadError: The session died while executing the > command. > + SSHTimeoutError: The command execution timed out. > + """ > try: > - self._clean_session() > - if env: > - command =3D f"{env} {command}" > - self._send_line(command) > - except Exception as e: > - raise e > + output =3D self.session.run( > + command, env=3Denv, warn=3DTrue, hide=3DTrue, timeout=3D= timeout > + ) > > - output =3D self.get_output(timeout=3Dtimeout) > - self.session.PROMPT =3D self.session.UNIQUE_PROMPT > - self.session.prompt(0.1) > + except (UnexpectedExit, ThreadException) as e: > + self._logger.exception(e) > + raise SSHSessionDeadError(self.hostname) from e > > - return output > + except CommandTimedOut as e: > + self._logger.exception(e) > + raise SSHTimeoutError(command, e.result.stderr) from e > > - def _close(self, force: bool =3D False) -> None: > - if force is True: > - self.session.close() > - else: > - if self.is_alive(): > - self.session.logout() > + return CommandResult( > + self.name, command, output.stdout, output.stderr, > output.return_code > + ) > > - def copy_file( > + def copy_from( > self, > source_file: str | PurePath, > destination_file: str | PurePath, > - source_remote: bool =3D False, > ) -> None: > - """ > - Send a local file to a remote host. > - """ > - if source_remote: > - source_file =3D f"{self.username}@{self.ip}:{source_file}" > - else: > - destination_file =3D f"{self.username}@ > {self.ip}:{destination_file}" > + self.session.get(str(destination_file), str(source_file)) > > - port =3D "" > - if self.port: > - port =3D f" -P {self.port}" > - > - command =3D ( > - f"scp -v{port} -o NoHostAuthenticationForLocalhost=3Dyes" > - f" {source_file} {destination_file}" > - ) > - > - self._spawn_scp(command) > + def copy_to( > + self, > + source_file: str | PurePath, > + destination_file: str | PurePath, > + ) -> None: > + self.session.put(str(source_file), str(destination_file)) > > - def _spawn_scp(self, scp_cmd: str) -> None: > - """ > - Transfer a file with SCP > - """ > - self._logger.info(scp_cmd) > - p: pexpect.spawn =3D pexpect.spawn(scp_cmd) > - time.sleep(0.5) > - ssh_newkey: str =3D "Are you sure you want to continue connectin= g" > - i: int =3D p.expect( > - [ssh_newkey, "[pP]assword", "# ", pexpect.EOF, > pexpect.TIMEOUT], 120 > - ) > - if i =3D=3D 0: # add once in trust list > - p.sendline("yes") > - i =3D p.expect([ssh_newkey, "[pP]assword", pexpect.EOF], 2) > - > - if i =3D=3D 1: > - time.sleep(0.5) > - p.sendline(self.password) > - p.expect("Exit status 0", 60) > - if i =3D=3D 4: > - self._logger.error("SCP TIMEOUT error %d" % i) > - p.close() > + def _close(self, force: bool =3D False) -> None: > + self.session.close() > diff --git a/dts/framework/testbed_model/sut_node.py > b/dts/framework/testbed_model/sut_node.py > index 2b2b50d982..9dbc390848 100644 > --- a/dts/framework/testbed_model/sut_node.py > +++ b/dts/framework/testbed_model/sut_node.py > @@ -10,7 +10,7 @@ > from framework.config import BuildTargetConfiguration, NodeConfiguration > from framework.remote_session import CommandResult, OSSession > from framework.settings import SETTINGS > -from framework.utils import EnvVarsDict, MesonArgs > +from framework.utils import MesonArgs > > from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice > from .node import Node > @@ -27,7 +27,7 @@ class SutNode(Node): > _dpdk_prefix_list: list[str] > _dpdk_timestamp: str > _build_target_config: BuildTargetConfiguration | None > - _env_vars: EnvVarsDict > + _env_vars: dict > _remote_tmp_dir: PurePath > __remote_dpdk_dir: PurePath | None > _dpdk_version: str | None > @@ -38,7 +38,7 @@ 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._env_vars =3D {} > self._remote_tmp_dir =3D self.main_session.get_remote_tmp_dir() > self.__remote_dpdk_dir =3D None > self._dpdk_version =3D None > @@ -94,7 +94,7 @@ def _configure_build_target( > """ > Populate common environment variables and set build target confi= g. > """ > - self._env_vars =3D EnvVarsDict() > + self._env_vars =3D {} > self._build_target_config =3D build_target_config > self._env_vars.update( > > self.main_session.get_dpdk_build_env_vars(build_target_config.arch) > @@ -112,7 +112,7 @@ def _copy_dpdk_tarball(self) -> None: > Copy to and extract DPDK tarball on the SUT node. > """ > self._logger.info("Copying DPDK tarball to SUT.") > - self.main_session.copy_file(SETTINGS.dpdk_tarball_path, > self._remote_tmp_dir) > + self.main_session.copy_to(SETTINGS.dpdk_tarball_path, > self._remote_tmp_dir) > > # construct remote tarball path > # the basename is the same on local host and on remote Node > @@ -259,7 +259,7 @@ def run_dpdk_app( > Run DPDK application on the remote node. > """ > return self.main_session.send_command( > - f"{app_path} {eal_args}", timeout, verify=3DTrue > + f"{app_path} {eal_args}", timeout, privileged=3DTrue, > verify=3DTrue > ) > > > diff --git a/dts/framework/utils.py b/dts/framework/utils.py > index 55e0b0ef0e..8cfbc6a29d 100644 > --- a/dts/framework/utils.py > +++ b/dts/framework/utils.py > @@ -42,19 +42,10 @@ def expand_range(range_str: str) -> list[int]: > return expanded_range > > > -def GREEN(text: str) -> str: > - return f"\u001B[32;1m{str(text)}\u001B[0m" > - > - > def RED(text: str) -> str: > return f"\u001B[31;1m{str(text)}\u001B[0m" > > > -class EnvVarsDict(dict): > - def __str__(self) -> str: > - return " ".join(["=3D".join(item) for item in self.items()]) > - > - > class MesonArgs(object): > """ > Aggregate the arguments needed to build DPDK: > diff --git a/dts/poetry.lock b/dts/poetry.lock > index 0b2a007d4d..2438f337cd 100644 > --- a/dts/poetry.lock > +++ b/dts/poetry.lock > @@ -12,6 +12,18 @@ docs =3D ["furo", "sphinx", "zope.interface", > "sphinx-notfound-page"] > tests =3D ["coverage[toml] (>=3D5.0.2)", "hypothesis", "pympler", "pytes= t > (>=3D4.3.0)", "mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins", > "zope.interface", "cloudpickle"] > tests_no_zope =3D ["coverage[toml] (>=3D5.0.2)", "hypothesis", "pympler"= , > "pytest (>=3D4.3.0)", "mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins"= , > "cloudpickle"] > > +[[package]] > +name =3D "bcrypt" > +version =3D "4.0.1" > +description =3D "Modern password hashing for your software and your serv= ers" > +category =3D "main" > +optional =3D false > +python-versions =3D ">=3D3.6" > + > +[package.extras] > +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)"] > +typecheck =3D ["mypy"] > + > [[package]] > name =3D "black" > version =3D "22.10.0" > @@ -33,6 +45,17 @@ d =3D ["aiohttp (>=3D3.7.4)"] > jupyter =3D ["ipython (>=3D7.8.0)", "tokenize-rt (>=3D3.2.0)"] > uvloop =3D ["uvloop (>=3D0.15.2)"] > > +[[package]] > +name =3D "cffi" > +version =3D "1.15.1" > +description =3D "Foreign Function Interface for Python calling C code." > +category =3D "main" > +optional =3D false > +python-versions =3D "*" > + > +[package.dependencies] > +pycparser =3D "*" > + > [[package]] > name =3D "click" > version =3D "8.1.3" > @@ -52,6 +75,52 @@ category =3D "dev" > optional =3D false > python-versions =3D > "!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D3.3.*,!=3D3.4.*,!=3D3.5.*,!=3D3.6.*,>= =3D2.7" > > +[[package]] > +name =3D "cryptography" > +version =3D "40.0.2" > +description =3D "cryptography is a package which provides cryptographic > recipes and primitives to Python developers." > +category =3D "main" > +optional =3D false > +python-versions =3D ">=3D3.6" > + > +[package.dependencies] > +cffi =3D ">=3D1.12" > + > +[package.extras] > +docs =3D ["sphinx (>=3D5.3.0)", "sphinx-rtd-theme (>=3D1.1.1)"] > +docstest =3D ["pyenchant (>=3D1.6.11)", "twine (>=3D1.12.0)", > "sphinxcontrib-spelling (>=3D4.0.1)"] > +pep8test =3D ["black", "ruff", "mypy", "check-manifest"] > +sdist =3D ["setuptools-rust (>=3D0.11.4)"] > +ssh =3D ["bcrypt (>=3D3.1.5)"] > +test =3D ["pytest (>=3D6.2.0)", "pytest-shard (>=3D0.1.2)", "pytest-benc= hmark", > "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601"] > +test-randomorder =3D ["pytest-randomly"] > +tox =3D ["tox"] > + > +[[package]] > +name =3D "fabric" > +version =3D "2.7.1" > +description =3D "High level SSH command execution" > +category =3D "main" > +optional =3D false > +python-versions =3D "*" > + > +[package.dependencies] > +invoke =3D ">=3D1.3,<2.0" > +paramiko =3D ">=3D2.4" > +pathlib2 =3D "*" > + > +[package.extras] > +pytest =3D ["mock (>=3D2.0.0,<3.0)", "pytest (>=3D3.2.5,<4.0)"] > +testing =3D ["mock (>=3D2.0.0,<3.0)"] > + > +[[package]] > +name =3D "invoke" > +version =3D "1.7.3" > +description =3D "Pythonic task execution" > +category =3D "main" > +optional =3D false > +python-versions =3D "*" > + > [[package]] > name =3D "isort" > version =3D "5.10.1" > @@ -136,23 +205,41 @@ optional =3D false > python-versions =3D "*" > > [[package]] > -name =3D "pathspec" > -version =3D "0.10.1" > -description =3D "Utility library for gitignore style pattern matching of > file paths." > -category =3D "dev" > +name =3D "paramiko" > +version =3D "3.1.0" > +description =3D "SSH2 protocol library" > +category =3D "main" > optional =3D false > -python-versions =3D ">=3D3.7" > +python-versions =3D ">=3D3.6" > + > +[package.dependencies] > +bcrypt =3D ">=3D3.2" > +cryptography =3D ">=3D3.3" > +pynacl =3D ">=3D1.5" > + > +[package.extras] > +all =3D ["pyasn1 (>=3D0.1.7)", "invoke (>=3D2.0)", "gssapi (>=3D1.4.1)",= "pywin32 > (>=3D2.1.8)"] > +gssapi =3D ["pyasn1 (>=3D0.1.7)", "gssapi (>=3D1.4.1)", "pywin32 (>=3D2.= 1.8)"] > +invoke =3D ["invoke (>=3D2.0)"] > > [[package]] > -name =3D "pexpect" > -version =3D "4.8.0" > -description =3D "Pexpect allows easy control of interactive console > applications." > +name =3D "pathlib2" > +version =3D "2.3.7.post1" > +description =3D "Object-oriented filesystem paths" > category =3D "main" > optional =3D false > python-versions =3D "*" > > [package.dependencies] > -ptyprocess =3D ">=3D0.5" > +six =3D "*" > + > +[[package]] > +name =3D "pathspec" > +version =3D "0.10.1" > +description =3D "Utility library for gitignore style pattern matching of > file paths." > +category =3D "dev" > +optional =3D false > +python-versions =3D ">=3D3.7" > > [[package]] > name =3D "platformdirs" > @@ -166,14 +253,6 @@ python-versions =3D ">=3D3.7" > docs =3D ["furo (>=3D2021.7.5b38)", "proselint (>=3D0.10.2)", > "sphinx-autodoc-typehints (>=3D1.12)", "sphinx (>=3D4)"] > test =3D ["appdirs (=3D=3D1.4.4)", "pytest-cov (>=3D2.7)", "pytest-mock = (>=3D3.6)", > "pytest (>=3D6)"] > > -[[package]] > -name =3D "ptyprocess" > -version =3D "0.7.0" > -description =3D "Run a subprocess in a pseudo terminal" > -category =3D "main" > -optional =3D false > -python-versions =3D "*" > - > [[package]] > name =3D "pycodestyle" > version =3D "2.9.1" > @@ -182,6 +261,14 @@ category =3D "dev" > optional =3D false > python-versions =3D ">=3D3.6" > > +[[package]] > +name =3D "pycparser" > +version =3D "2.21" > +description =3D "C parser in Python" > +category =3D "main" > +optional =3D false > +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*, !=3D3.3.*= " > + > [[package]] > name =3D "pydocstyle" > version =3D "6.1.1" > @@ -228,6 +315,21 @@ tests =3D ["pytest (>=3D7.1.2)", "pytest-mypy", > "eradicate (>=3D2.0.0)", "radon (>=3D5.1 > toml =3D ["toml (>=3D0.10.2)"] > vulture =3D ["vulture"] > > +[[package]] > +name =3D "pynacl" > +version =3D "1.5.0" > +description =3D "Python binding to the Networking and Cryptography (NaCl= ) > library" > +category =3D "main" > +optional =3D false > +python-versions =3D ">=3D3.6" > + > +[package.dependencies] > +cffi =3D ">=3D1.4.1" > + > +[package.extras] > +docs =3D ["sphinx (>=3D1.6.5)", "sphinx-rtd-theme"] > +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)", "hypothesis (>=3D3.27.0)"] > + > [[package]] > name =3D "pyrsistent" > version =3D "0.19.1" > @@ -244,6 +346,14 @@ category =3D "main" > optional =3D false > python-versions =3D ">=3D3.6" > > +[[package]] > +name =3D "six" > +version =3D "1.16.0" > +description =3D "Python 2 and 3 compatibility utilities" > +category =3D "main" > +optional =3D false > +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*" > + > [[package]] > name =3D "snowballstemmer" > version =3D "2.2.0" > @@ -299,13 +409,18 @@ jsonschema =3D ">=3D4,<5" > [metadata] > lock-version =3D "1.1" > python-versions =3D "^3.10" > -content-hash =3D > "a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb9e260a67e92c2403e2319f" > +content-hash =3D > "719c43bcaa5d181921debda884f8f714063df0b2336d61e9f64ecab034e8b139" > > [metadata.files] > attrs =3D [] > +bcrypt =3D [] > black =3D [] > +cffi =3D [] > click =3D [] > colorama =3D [] > +cryptography =3D [] > +fabric =3D [] > +invoke =3D [] > isort =3D [] > jsonpatch =3D [] > jsonpointer =3D [] > @@ -313,22 +428,22 @@ jsonschema =3D [] > mccabe =3D [] > mypy =3D [] > mypy-extensions =3D [] > +paramiko =3D [] > +pathlib2 =3D [] > pathspec =3D [] > -pexpect =3D [ > - {file =3D "pexpect-4.8.0-py2.py3-none-any.whl", hash =3D > "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"= }, > - {file =3D "pexpect-4.8.0.tar.gz", hash =3D > "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"= }, > -] > platformdirs =3D [ > {file =3D "platformdirs-2.5.2-py3-none-any.whl", hash =3D > "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"= }, > {file =3D "platformdirs-2.5.2.tar.gz", hash =3D > "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"= }, > ] > -ptyprocess =3D [] > pycodestyle =3D [] > +pycparser =3D [] > pydocstyle =3D [] > pyflakes =3D [] > pylama =3D [] > +pynacl =3D [] > pyrsistent =3D [] > pyyaml =3D [] > +six =3D [] > snowballstemmer =3D [] > toml =3D [] > tomli =3D [] > diff --git a/dts/pyproject.toml b/dts/pyproject.toml > index a136c91e5e..50bcdb327a 100644 > --- a/dts/pyproject.toml > +++ b/dts/pyproject.toml > @@ -9,10 +9,10 @@ authors =3D ["Owen Hilyard ", " > dts@dpdk.org"] > > [tool.poetry.dependencies] > python =3D "^3.10" > -pexpect =3D "^4.8.0" > warlock =3D "^2.0.1" > PyYAML =3D "^6.0" > types-PyYAML =3D "^6.0.8" > +fabric =3D "^2.7.1" > > [tool.poetry.dev-dependencies] > mypy =3D "^0.961" > -- > 2.34.1 > > --00000000000079a8d305fea80193 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Acked-by: Jeremy Spewock <jspewock@iol.unh.edu>

On Fri, Jun 9,= 2023 at 5:46=E2=80=AFAM Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech&g= t; wrote:
Pexpec= t is not a dedicated SSH connection library while Fabric is. With
Fabric, all SSH-related logic is provided and we can just focus on
what's DTS specific.

Signed-off-by: Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech>
---

Notes:
=C2=A0 =C2=A0 v3: updated passwordless sudo setup on Linux

=C2=A0doc/guides/tools/dts.rst=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 29 +-
=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= =A02 +-
=C2=A0dts/framework/exception.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 10 +-
=C2=A0dts/framework/remote_session/linux_session.py |=C2=A0 31 +-
=C2=A0dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2=A0 51 ++= +-
=C2=A0dts/framework/remote_session/posix_session.py |=C2=A0 48 +--
=C2=A0.../remote_session/remote/remote_session.py=C2=A0 =C2=A0|=C2=A0 35 ++= -
=C2=A0.../remote_session/remote/ssh_session.py=C2=A0 =C2=A0 =C2=A0 | 287 ++= ++++------------
=C2=A0dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 =C2=A0|= =C2=A0 12 +-
=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 =C2=A09 -
=C2=A0dts/poetry.lock=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| 161 ++++++++--=
=C2=A0dts/pyproject.toml=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=A02 +-
=C2=A012 files changed, 376 insertions(+), 301 deletions(-)

diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
index ebd6dceb6a..c7b31623e4 100644
--- a/doc/guides/tools/dts.rst
+++ b/doc/guides/tools/dts.rst
@@ -95,9 +95,14 @@ Setting up DTS environment

=C2=A0#. **SSH Connection**

-=C2=A0 =C2=A0DTS uses Python pexpect for SSH connections between DTS envir= onment and the other hosts.
-=C2=A0 =C2=A0The pexpect implementation is a wrapper around the ssh comman= d in the DTS environment.
-=C2=A0 =C2=A0This means it'll use the SSH agent providing the ssh comm= and and its keys.
+=C2=A0 =C2=A0DTS uses the Fabric Python library for SSH connections betwee= n DTS environment
+=C2=A0 =C2=A0and the other hosts.
+=C2=A0 =C2=A0The authentication method used is pubkey authentication.
+=C2=A0 =C2=A0Fabric tries to use a passed key/certificate,
+=C2=A0 =C2=A0then any key it can with through an SSH agent,
+=C2=A0 =C2=A0then any "id_rsa", "id_dsa" or "id_e= cdsa" key discoverable in ``~/.ssh/``
+=C2=A0 =C2=A0(with any matching OpenSSH-style certificates).
+=C2=A0 =C2=A0DTS doesn't pass any keys, so Fabric tries to use the oth= er two methods.


=C2=A0Setting up System Under Test
@@ -132,6 +137,21 @@ There are two areas that need to be set up on a System= Under Test:
=C2=A0 =C2=A0 =C2=A0 It's possible to use the hugepage configuration al= ready present on the SUT.
=C2=A0 =C2=A0 =C2=A0 If you wish to do so, don't specify the hugepage c= onfiguration in the DTS config file.

+#. **User with administrator privileges**
+
+.. _sut_admin_user:
+
+=C2=A0 =C2=A0DTS needs administrator privileges to run DPDK applications (= such as testpmd) on the SUT.
+=C2=A0 =C2=A0The SUT user must be able run commands in privileged mode wit= hout asking for password.
+=C2=A0 =C2=A0On most Linux distributions, it's a matter of setting up = passwordless sudo:
+
+=C2=A0 =C2=A0#. Run ``sudo visudo`` and check that it contains ``%sudo=C2= =A0 =C2=A0ALL=3D(ALL:ALL) NOPASSWD:ALL``.
+
+=C2=A0 =C2=A0#. Add the SUT user to the sudo group with:
+
+=C2=A0 =C2=A0.. code-block:: console
+
+=C2=A0 =C2=A0 =C2=A0 sudo usermod -aG sudo <sut_user>

=C2=A0Running DTS
=C2=A0-----------
@@ -151,7 +171,8 @@ which is a template that illustrates what can be config= ured in DTS:
=C2=A0 =C2=A0 =C2=A0 :start-at: executions:


-The user must be root or any other user with prompt starting with ``#``. +The user must have :ref:`administrator privileges <sut_admin_user>`<= br> +which don't require password authentication.
=C2=A0The other fields are mostly self-explanatory
=C2=A0and documented in more detail in ``dts/framework/config/conf_yaml_sch= ema.json``.

diff --git a/dts/conf.yaml b/dts/conf.yaml
index a9bd8a3ecf..129801d87c 100644
--- a/dts/conf.yaml
+++ b/dts/conf.yaml
@@ -16,7 +16,7 @@ executions:
=C2=A0nodes:
=C2=A0 =C2=A0- name: "SUT 1"
=C2=A0 =C2=A0 =C2=A0hostname: sut1.change.me.localhost
-=C2=A0 =C2=A0 user: root
+=C2=A0 =C2=A0 user: dtsuser
=C2=A0 =C2=A0 =C2=A0arch: x86_64
=C2=A0 =C2=A0 =C2=A0os: linux
=C2=A0 =C2=A0 =C2=A0lcores: ""
diff --git a/dts/framework/exception.py b/dts/framework/exception.py
index ca353d98fc..44ff4e979a 100644
--- a/dts/framework/exception.py
+++ b/dts/framework/exception.py
@@ -62,13 +62,19 @@ class SSHConnectionError(DTSError):
=C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0host: str
+=C2=A0 =C2=A0 errors: list[str]
=C2=A0 =C2=A0 =C2=A0severity: ClassVar[ErrorSeverity] =3D ErrorSeverity.SSH= _ERR

-=C2=A0 =C2=A0 def __init__(self, host: str):
+=C2=A0 =C2=A0 def __init__(self, host: str, errors: list[str] | None =3D N= one):
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.host =3D host
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.errors =3D [] if errors is None else erro= rs

=C2=A0 =C2=A0 =C2=A0def __str__(self) -> str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"Error trying to connect with {se= lf.host}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 message =3D f"Error trying to connect wit= h {self.host}."
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.errors:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message +=3D f" Errors enco= untered while retrying: {', '.join(self.errors)}"
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return message


=C2=A0class SSHSessionDeadError(DTSError):
diff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/= remote_session/linux_session.py
index a1e3bc3a92..f13f399121 100644
--- a/dts/framework/remote_session/linux_session.py
+++ b/dts/framework/remote_session/linux_session.py
@@ -14,10 +14,11 @@ class LinuxSession(PosixSession):
=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_privileged_command(self, command: str) -> str: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"sudo -- sh -c '{command}'= ;"
+
=C2=A0 =C2=A0 =C2=A0def get_remote_cpus(self, use_first_core: bool) -> l= ist[LogicalCore]:
-=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 cpu_info =3D self.send_command("lscpu -p= =3DCPU,CORE,SOCKET,NODE|grep -v \\#").stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcores =3D []
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for cpu_line in cpu_info.splitlines(): =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcore, core, socket, node = =3D map(int, cpu_line.split(","))
@@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int, force_f= irst_numa: bool) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._mount_huge_pages()

=C2=A0 =C2=A0 =C2=A0def _get_hugepage_size(self) -> int:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.remote_session.send_com= mand(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"awk '/Hugepagesiz= e/ {print $2}' /proc/meminfo"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return int(hugepage_size)

=C2=A0 =C2=A0 =C2=A0def _get_hugepages_total(self) -> int:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.remote_session.send_c= ommand(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"awk '/HugePages_T= otal/ { print $2 }' /proc/meminfo"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return int(hugepages_total)

=C2=A0 =C2=A0 =C2=A0def _get_numa_nodes(self) -> list[int]:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.remote_sessi= on.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.send_command= (
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"cat /sy= s/devices/system/node/online", verify=3DTrue
=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=A0numa_range =3D expand_range= (numa_count)
@@ -70,14 +71,12 @@ def _get_numa_nodes(self) -> list[int]:
=C2=A0 =C2=A0 =C2=A0def _mount_huge_pages(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info("Re-mounting Hugepag= es.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hugapge_fs_cmd =3D "awk '/hugetl= bfs/ { print $2 }' /proc/mounts"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f"umount= $({hugapge_fs_cmd})")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(hu= gapge_fs_cmd)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"umount $({hugapge_fs_= cmd})")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(hugapge_fs_cmd) =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if result.stdout =3D=3D "":
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_mount_path =3D "= ;/mnt/huge"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command= (f"mkdir -p {remote_mount_path}")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command= (
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"mount -t hu= getlbfs nodev {remote_mount_path}"
-=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.send_command(f"mkdir -= p {remote_mount_path}")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"mount -= t hugetlbfs nodev {remote_mount_path}")

=C2=A0 =C2=A0 =C2=A0def _supports_numa(self) -> bool:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# the system supports numa if self._numa_= nodes is non-empty and there are more
@@ -94,14 +93,12 @@ def _configure_huge_pages(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if force_first_numa and self._supports_nu= ma():
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# clear non-numa hugepages<= br> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command= (
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"echo 0 | su= do tee {hugepage_config_path}"
-=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.send_command(f"echo 0 = | tee {hugepage_config_path}", privileged=3DTrue)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hugepage_config_path =3D (<= br> =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"/sys/d= evices/system/node/node{self._numa_nodes[0]}/hugepages"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"/hugep= ages-{size}kB/nr_hugepages"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"echo {amount} | sudo tee = {hugepage_config_path}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"echo {amount} | tee {huge= page_config_path}", privileged=3DTrue
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
diff --git a/dts/framework/remote_session/os_session.py b/dts/framework/rem= ote_session/os_session.py
index 4c48ae2567..bfd70bd480 100644
--- a/dts/framework/remote_session/os_session.py
+++ b/dts/framework/remote_session/os_session.py
@@ -10,7 +10,7 @@
=C2=A0from framework.logger import DTSLOG
=C2=A0from framework.settings import SETTINGS
=C2=A0from framework.testbed_model import LogicalCore
-from framework.utils import EnvVarsDict, MesonArgs
+from framework.utils import MesonArgs

=C2=A0from .remote import CommandResult, RemoteSession, create_remote_sessi= on

@@ -53,17 +53,32 @@ def is_alive(self) -> bool:
=C2=A0 =C2=A0 =C2=A0def send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0command: str,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged: bool =3D False,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0verify: bool =3D False,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,
=C2=A0 =C2=A0 =C2=A0) -> CommandResult:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0An all-purpose API in case the command to= be executed is already
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0OS-agnostic, such as when the path to the= executed command has been
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0constructed beforehand.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if privileged:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D self._get_privileged= _command(command)
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.remote_session.send_command(c= ommand, timeout, verify, env)

+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -> str: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Modify the command so that i= t executes with administrative privileges.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to modify.<= br> +
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The modified command that execut= es with administrative privileges.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+
=C2=A0 =C2=A0 =C2=A0@abstractmethod
=C2=A0 =C2=A0 =C2=A0def guess_dpdk_remote_dir(self, remote_dir) -> PureP= ath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
@@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PurePath) ->= ; PurePath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0@abstractmethod
-=C2=A0 =C2=A0 def copy_file(
+=C2=A0 =C2=A0 def copy_from(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
=C2=A0 =C2=A0 =C2=A0) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from the remote = Node to the local filesystem.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node associat= ed with this remote
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the local files= ystem.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the rem= ote Node.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire= ctory path on the local filesystem.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def copy_to(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from local files= ystem to the remote Node.
+
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Copy source_file from local filesystem to= destination_file
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with the remote = session.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the directio= n - copy source_file from the
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated remote Node to destination_file on = local storage.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with this remote= session.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the loc= al filesystem.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire= ctory path on the remote Node.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0@abstractmethod
@@ -128,7 +161,7 @@ def extract_remote_tarball(
=C2=A0 =C2=A0 =C2=A0@abstractmethod
=C2=A0 =C2=A0 =C2=A0def build_dpdk(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0meson_args: MesonArgs,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_dir: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_build_dir: str | PurePath, diff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/= remote_session/posix_session.py
index d38062e8d6..8ca0acb429 100644
--- a/dts/framework/remote_session/posix_session.py
+++ b/dts/framework/remote_session/posix_session.py
@@ -9,7 +9,7 @@
=C2=A0from framework.config import Architecture
=C2=A0from framework.exception import DPDKBuildError, RemoteCommandExecutio= nError
=C2=A0from framework.settings import SETTINGS
-from framework.utils import EnvVarsDict, MesonArgs
+from framework.utils import MesonArgs

=C2=A0from .os_session import OSSession

@@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -> str:

=C2=A0 =C2=A0 =C2=A0def guess_dpdk_remote_dir(self, remote_dir) -> PureP= osixPath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_guess =3D self.join_remote_path(re= mote_dir, "dpdk-*")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(f&= quot;ls -d {remote_guess} | tail -1")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f"ls -d {rem= ote_guess} | tail -1")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return PurePosixPath(result.stdout)

=C2=A0 =C2=A0 =C2=A0def get_remote_tmp_dir(self) -> PurePosixPath:
@@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architecture) -&g= t; dict:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0env_vars =3D {}
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if arch =3D=3D Architecture.i686:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# find the pkg-config path = and store it in PKG_CONFIG_LIBDIR
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send= _command("find /usr -type d -name pkgconfig")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command("= find /usr -type d -name pkgconfig")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0pkg_path =3D "" =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0res_path =3D out.stdout.spl= it("\r\n")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for cur_path in res_path: @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Architecture) -= > dict:
=C2=A0 =C2=A0 =C2=A0def join_remote_path(self, *args: str | PurePath) ->= PurePosixPath:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return PurePosixPath(*args)

-=C2=A0 =C2=A0 def copy_file(
+=C2=A0 =C2=A0 def copy_from(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
=C2=A0 =C2=A0 =C2=A0) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_file(source_file, des= tination_file, source_remote)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_from(source_file, des= tination_file)
+
+=C2=A0 =C2=A0 def copy_to(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_to(source_file, desti= nation_file)

=C2=A0 =C2=A0 =C2=A0def remove_remote_dir(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
@@ -80,24 +86,24 @@ def remove_remote_dir(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0force: bool =3D True,
=C2=A0 =C2=A0 =C2=A0) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0opts =3D PosixSession.combine_short_optio= ns(r=3Drecursive, f=3Dforce)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f"rm{opt= s} {remote_dir_path}")
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"rm{opts} {remote_dir_= path}")

=C2=A0 =C2=A0 =C2=A0def extract_remote_tarball(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_tarball_path: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0expected_dir: str | PurePath | None =3D N= one,
=C2=A0 =C2=A0 =C2=A0) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"tar xfm {remote_tarb= all_path} "
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"-C {PurePosixPath(re= mote_tarball_path).parent}",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A060,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if expected_dir:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command= (f"ls {expected_dir}", verify=3DTrue)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"ls {exp= ected_dir}", verify=3DTrue)

=C2=A0 =C2=A0 =C2=A0def build_dpdk(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0meson_args: MesonArgs,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_dir: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_build_dir: str | PurePath, @@ -108,7 +114,7 @@ def build_dpdk(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if rebuild:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# reconfigure= , then build
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info("Reconfiguring DPDK build.")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_sessio= n.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0f"meson configure {meson_args} {remote_dpdk_build_dir}",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0timeout,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0verify=3DTrue,
@@ -118,7 +124,7 @@ def build_dpdk(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# fresh build= - remove target dir first, then build from scratch
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._
logger.info("Configuring DPDK build from scratch.")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.remove_r= emote_dir(remote_dpdk_build_dir)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_sessio= n.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0f"meson setup "
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0f"{meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}",
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0timeout,
@@ -127,14 +133,14 @@ def build_dpdk(
=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=A0self._
logger.info("Buildi= ng DPDK.")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command= (
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"ninja = -C {remote_dpdk_build_dir}", timeout, verify=3DTrue, env=3Denv_vars =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0except RemoteCommandExecutionError as e:<= br> =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0raise DPDKBuildError(f"= ;DPDK build failed when doing '{e.command}'.")

=C2=A0 =C2=A0 =C2=A0def get_dpdk_version(self, build_dir: str | PurePath) -= > str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_command(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(
=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
@@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iter= able[str]) -> None:
=C2=A0 =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 =C2=A0dpdk_pids =3D self._get_dpd= k_pids(dpdk_runtime_dirs)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for dpdk_pid in dpdk_pids:<= br> -=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 =C2=A0 =C2=A0 self.send_command(= f"kill -9 {dpdk_pid}", 20)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._check_dpdk_hugepages(= dpdk_runtime_dirs)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remove_dpdk_runtime_d= irs(dpdk_runtime_dirs)

@@ -168,7 +174,7 @@ def _list_remote_dirs(self, remote_path: str | PurePath= ) -> list[str] | None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Return a list of directories of the remot= e_dir.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0If remote_path doesn't exist, return = None.
=C2=A0 =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 out =3D self.send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f"ls -l {remote_path} = | awk '/^d/ {{print $NF}}'"
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if "No such file or directory" = in out:
@@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[st= r | PurePath]) -> list[in
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for dpdk_runtime_dir in dpdk_runtime_dirs= :
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0dpdk_config_file =3D PurePo= sixPath(dpdk_runtime_dir, "config")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._remote_files_exist= s(dpdk_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 out =3D self.send_= command(f"lsof -Fp {dpdk_config_file}").stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if out and &q= uot;No 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 =C2= =A0for 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 =C2=A0match =3D re.match(pid_regex, out_line)
@@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[st= r | PurePath]) -> list[in
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return pids

=C2=A0 =C2=A0 =C2=A0def _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 result =3D self.send_command(f"test -e {r= emote_path}")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return not result.return_code

=C2=A0 =C2=A0 =C2=A0def _check_dpdk_hugepages(
@@ -202,9 +206,7 @@ def _check_dpdk_hugepages(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for dpdk_runtime_dir in dpdk_runtime_dirs= :
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hugepage_info =3D PurePosix= Path(dpdk_runtime_dir, "hugepage_info")
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._remote_files_exist= s(hugepage_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 out =3D self.send_= command(f"lsof -Fp {hugepage_info}").stdout
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if out and &q= uot;No 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 =C2= =A0self._logger.warning("Some DPDK processes did not free hugepages.&q= uot;)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0self._logger.warning("*******************************************&q= uot;)
diff --git a/dts/framework/remote_session/remote/remote_session.py b/dts/fr= amework/remote_session/remote/remote_session.py
index 91dee3cb4f..0647d93de4 100644
--- a/dts/framework/remote_session/remote/remote_session.py
+++ b/dts/framework/remote_session/remote/remote_session.py
@@ -11,7 +11,6 @@
=C2=A0from framework.exception import RemoteCommandExecutionError
=C2=A0from framework.logger import DTSLOG
=C2=A0from framework.settings import SETTINGS
-from framework.utils import EnvVarsDict


=C2=A0@dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)
@@ -89,7 +88,7 @@ def send_command(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0command: str,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0timeout: float =3D SETTINGS.timeout,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0verify: bool =3D False,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,
=C2=A0 =C2=A0 =C2=A0) -> CommandResult:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Send a command to the connected node usin= g optional env vars
@@ -114,7 +113,7 @@ def send_command(

=C2=A0 =C2=A0 =C2=A0@abstractmethod
=C2=A0 =C2=A0 =C2=A0def _send_command(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa= rsDict | None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: dict = | None
=C2=A0 =C2=A0 =C2=A0) -> CommandResult:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Use the underlying protocol to execute th= e command using optional env vars
@@ -141,15 +140,33 @@ def is_alive(self) -> bool:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""

=C2=A0 =C2=A0 =C2=A0@abstractmethod
-=C2=A0 =C2=A0 def copy_file(
+=C2=A0 =C2=A0 def copy_from(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
=C2=A0 =C2=A0 =C2=A0) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from the remote = Node to the local filesystem.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node associat= ed with this remote
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the local files= ystem.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the rem= ote Node.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire= ctory path on the local filesystem.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesystem to dest= ination_file on the remote Node
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated with the remote session.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the directio= n - copy source_file from the
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated Node to destination_file on local f= ilesystem.
+
+=C2=A0 =C2=A0 @abstractmethod
+=C2=A0 =C2=A0 def copy_to(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from local files= ystem to the remote Node.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesystem to dest= ination_file
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with this remote= session.
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the loc= al filesystem.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire= ctory path on the remote Node.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
diff --git a/dts/framework/remote_session/remote/ssh_session.py b/dts/frame= work/remote_session/remote/ssh_session.py
index 42ff9498a2..8d127f1601 100644
--- a/dts/framework/remote_session/remote/ssh_session.py
+++ b/dts/framework/remote_session/remote/ssh_session.py
@@ -1,29 +1,49 @@
=C2=A0# SPDX-License-Identifier: BSD-3-Clause
-# Copyright(c) 2010-2014 Intel Corporation
-# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
-# Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2023 PANTHEON.tech s.r.o.

-import time
+import socket
+import traceback
=C2=A0from pathlib import PurePath

-import pexpect=C2=A0 # type: ignore
-from pexpect import pxssh=C2=A0 # type: ignore
+from fabric import Connection=C2=A0 # type: ignore[import]
+from invoke.exceptions import (=C2=A0 # type: ignore[import]
+=C2=A0 =C2=A0 CommandTimedOut,
+=C2=A0 =C2=A0 ThreadException,
+=C2=A0 =C2=A0 UnexpectedExit,
+)
+from paramiko.ssh_exception import (=C2=A0 # type: ignore[import]
+=C2=A0 =C2=A0 AuthenticationException,
+=C2=A0 =C2=A0 BadHostKeyException,
+=C2=A0 =C2=A0 NoValidConnectionsError,
+=C2=A0 =C2=A0 SSHException,
+)

=C2=A0from framework.config import NodeConfiguration
=C2=A0from framework.exception import SSHConnectionError, SSHSessionDeadErr= or, SSHTimeoutError
=C2=A0from framework.logger import DTSLOG
-from framework.utils import GREEN, RED, EnvVarsDict

=C2=A0from .remote_session import CommandResult, RemoteSession


=C2=A0class SSHSession(RemoteSession):
-=C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 Module for creating Pexpect SSH remote sessions.
+=C2=A0 =C2=A0 """A persistent SSH connection to a remote No= de.
+
+=C2=A0 =C2=A0 The connection is implemented with the Fabric Python library= .
+
+=C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The configuration of the Node to = connect to.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: The name of the session.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: The logger used for logging.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 This should be passed from the p= arent OSSession.
+
+=C2=A0 =C2=A0 Attributes:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session: The underlying Fabric SSH connection.=
+
+=C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHConnectionError: The connection cannot be e= stablished.
=C2=A0 =C2=A0 =C2=A0"""

-=C2=A0 =C2=A0 session: pxssh.pxssh
-=C2=A0 =C2=A0 magic_prompt: str
+=C2=A0 =C2=A0 session: Connection

=C2=A0 =C2=A0 =C2=A0def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
@@ -31,218 +51,91 @@ def __init__(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0session_name: str,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0logger: DTSLOG,
=C2=A0 =C2=A0 =C2=A0):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.magic_prompt =3D "MAGIC PROMPT"=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(SSHSession, self).__init__(node_con= fig, session_name, logger)

=C2=A0 =C2=A0 =C2=A0def _connect(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Create connection to assigned node.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 errors =3D []
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0retry_attempts =3D 10
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0login_timeout =3D 20 if self.port else 10=
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 password_regex =3D (
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 r"(?i)(?:password:)|(?:pass= phrase for key)|(?i)(password for .+:)"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry= _attempts):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session =3D p= xssh.pxssh(encoding=3D"utf-8")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .session.login(
-=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.ip,
-=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.username,
-=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.password,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 original_prompt=3D"[$#>]",
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 port=3Dself.port,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 login_timeout=3Dlogin_timeout,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 password_regex=3Dpassword_regex,
-=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=A0 =C2=A0 brea= k
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception a= s e:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= ._logger.warning(e)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time= .sleep(2)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= ._logge= r.info(
-=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"Retrying connection: retry number {retry_attempt + 1}.&qu= ot;
-=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 else:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise Exception(f&= quot;Connection to {self.hostname} failed")
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("stty -ech= o", "#")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("stty colu= mns 1000", "#")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("bind '= ;set enable-bracketed-paste off'", "#")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(RED(str(e)))<= br> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if getattr(self, "port"= ;, None):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 suggestion =3D ( -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;\nSuggestion: Check if the firewall on {self.hostname} is "
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;stopped.\n"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry_attempts): +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session =3D C= onnection(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= .ip,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 user= =3Dself.username,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port= =3Dself.port,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 conn= ect_kwargs=3D{"password": self.password},
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 conn= ect_timeout=3Dlogin_timeout,
=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 self._logger.info(GRE= EN(suggestion))
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionError(self.ho= stname)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.open(= )

-=C2=A0 =C2=A0 def send_expect(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, prompt: str, timeout: floa= t =3D 15, verify: bool =3D False
-=C2=A0 =C2=A0 ) -> str | int:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret =3D self.send_expect_base(co= mmand, prompt, timeout)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if verify:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret_status =3D sel= f.send_expect_base("echo $?", prompt, timeout)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 retv= al =3D int(ret_status)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if r= etval:
-=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._logger.error(f"Command: {command} failure!")
-=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._logger.error(ret)
-=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 retval
-=C2=A0 =C2=A0 =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 =C2= =A0 =C2=A0 return ret
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except ValueError:=
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 retu= rn ret
-=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 return ret
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Exception h= appened in [{command}] and output is "
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"[{self._get= _output()}]"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e
-
-=C2=A0 =C2=A0 def send_expect_base(self, command: str, prompt: str, timeou= t: float) -> str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 original_prompt =3D self.session.PROMPT
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D prompt
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prompt(command, timeout)
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D original_prompt
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
-
-=C2=A0 =C2=A0 def _clean_session(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.magic_prompt
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.get_output(timeout=3D0.01)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.UNIQUE_PR= OMPT
-
-=C2=A0 =C2=A0 def _send_line(self, command: str) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h= ostname)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(command) =3D=3D 2 and command.startswit= h("^"):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendcontrol(command= [1])
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendline(command) +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (ValueError, BadHostKeyEx= ception, AuthenticationException) as e:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.excep= tion(e)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectio= nError(self.hostname) from e

-=C2=A0 =C2=A0 def _prompt(self, command: str, timeout: float) -> None:<= br> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.session.prompt(timeout):
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(command, s= elf._get_output()) from None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (NoValidConnectionsError,= socket.error, SSHException) as e:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.debug= (traceback.format_exc())
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.warni= ng(e)

-=C2=A0 =C2=A0 def get_output(self, timeout: float =3D 15) -> str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get all output before timeout
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(timeout)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._flush()
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 error =3D repr(e)<= br> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if error not in er= rors:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 erro= rs.append(error)

-=C2=A0 =C2=A0 def _get_output(self) -> str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h= ostname)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self.session.before.rsplit("\r= \n", 1)[0]
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if before =3D=3D "[PEXPECT]":
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ""
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info( +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;Retrying connection: retry number {retry_attempt + 1}."
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )

-=C2=A0 =C2=A0 def _flush(self) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Clear all session buffer
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.buffer =3D ""
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.before =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 break
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionError(self.ho= stname, errors)

=C2=A0 =C2=A0 =C2=A0def is_alive(self) -> bool:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.isalive()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.is_connected

=C2=A0 =C2=A0 =C2=A0def _send_command(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa= rsDict | None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: dict = | None
=C2=A0 =C2=A0 =C2=A0) -> CommandResult:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self._send_command_get_output(comma= nd, timeout, env)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return_code =3D int(self._send_command_get_out= put("echo $?", timeout, None))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send a command and return th= e result of the execution.

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 # we're capturing only stdout
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(self.name, command, output, &q= uot;", return_code)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to execute.=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Wait at most this many = seconds for the execution to complete.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env: Extra environment variables= that will be used in command execution.

-=C2=A0 =C2=A0 def _send_command_get_output(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa= rsDict | None
-=C2=A0 =C2=A0 ) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHSessionDeadError: The session= died while executing the command.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHTimeoutError: The command exe= cution timed out.
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if env:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D f"= ;{env} {command}"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.session.run(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command, env=3Denv= , warn=3DTrue, hide=3DTrue, timeout=3Dtimeout
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.get_output(timeout=3Dtimeout)<= br> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.UNIQUE_PR= OMPT
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(0.1)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (UnexpectedExit, ThreadException) as e:=
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(e)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h= ostname) from e

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return output
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except CommandTimedOut as e:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(e)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(command, e= .result.stderr) from e

-=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if force is True:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.is_alive():
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.logou= t()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.name, command, output.stdout, ou= tput.stderr, output.return_code
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )

-=C2=A0 =C2=A0 def copy_file(
+=C2=A0 =C2=A0 def copy_from(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
=C2=A0 =C2=A0 =C2=A0) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a local file to a remote host.
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if source_remote:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file =3D f"{self.use= rname}@{self.ip}:{source_file}"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file =3D f"{sel= f.username}@{self.ip}:{destination_file}"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.get(str(destination_file), str(so= urce_file))

-=C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D ""
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.port:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D f" -P {self.port}&= quot;
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D (
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"scp -v{port} -o NoHostAut= henticationForLocalhost=3Dyes"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f" {source_file} {destinati= on_file}"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._spawn_scp(command)
+=C2=A0 =C2=A0 def copy_to(
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
+=C2=A0 =C2=A0 ) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.put(str(source_file), str(destina= tion_file))

-=C2=A0 =C2=A0 def _spawn_scp(self, scp_cmd: str) -> None:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Transfer a file with SCP
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(scp_cmd)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 p: pexpect.spawn =3D pexpect.spawn(scp_cmd) -=C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 ssh_newkey: str =3D "Are you sure you wan= t to continue connecting"
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 i: int =3D p.expect(
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [ssh_newkey, "[pP]assword&q= uot;, "# ", pexpect.EOF, pexpect.TIMEOUT], 120
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 0:=C2=A0 # add once in trust list<= br> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline("yes")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 i =3D p.expect([ssh_newkey, &quo= t;[pP]assword", pexpect.EOF], 2)
-
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 1:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline(self.password)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.expect("Exit status 0&quo= t;, 60)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 4:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error("SCP TIM= EOUT error %d" % i)
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 p.close()
+=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbe= d_model/sut_node.py
index 2b2b50d982..9dbc390848 100644
--- a/dts/framework/testbed_model/sut_node.py
+++ b/dts/framework/testbed_model/sut_node.py
@@ -10,7 +10,7 @@
=C2=A0from framework.config import BuildTargetConfiguration, NodeConfigurat= ion
=C2=A0from framework.remote_session import CommandResult, OSSession
=C2=A0from framework.settings import SETTINGS
-from framework.utils import EnvVarsDict, MesonArgs
+from framework.utils import MesonArgs

=C2=A0from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice
=C2=A0from .node import Node
@@ -27,7 +27,7 @@ class SutNode(Node):
=C2=A0 =C2=A0 =C2=A0_dpdk_prefix_list: list[str]
=C2=A0 =C2=A0 =C2=A0_dpdk_timestamp: str
=C2=A0 =C2=A0 =C2=A0_build_target_config: BuildTargetConfiguration | None -=C2=A0 =C2=A0 _env_vars: EnvVarsDict
+=C2=A0 =C2=A0 _env_vars: dict
=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_dpdk_version: str | None
@@ -38,7 +38,7 @@ def __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 =C2=A0self._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 self._env_vars =3D EnvVarsDict()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}
=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._dpdk_version =3D None
@@ -94,7 +94,7 @@ def _configure_build_target(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Populate common environment variables and= set build target config.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D build_targe= t_config
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars.update(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.main_session.get_dpdk_= build_env_vars(build_target_config.arch)
@@ -112,7 +112,7 @@ def _copy_dpdk_tarball(self) -> None:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Copy to and extract DPDK tarball on the S= UT node.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._logger.info("Copying DPDK tarbal= l to SUT.")
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_file(SETTINGS.dpdk_tarb= all_path, self._remote_tmp_dir)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_to(SETTINGS.dpdk_tarbal= l_path, self._remote_tmp_dir)

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# construct remote tarball path
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# the basename is the same on local host = and on remote Node
@@ -259,7 +259,7 @@ def run_dpdk_app(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Run DPDK application on the remote node.<= br> =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.send_command( -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{app_path} {eal_args}&quo= t;, timeout, verify=3DTrue
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{app_path} {eal_args}&quo= t;, timeout, privileged=3DTrue, verify=3DTrue
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)


diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 55e0b0ef0e..8cfbc6a29d 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -42,19 +42,10 @@ def expand_range(range_str: str) -> list[int]:
=C2=A0 =C2=A0 =C2=A0return expanded_range


-def GREEN(text: str) -> str:
-=C2=A0 =C2=A0 return f"\u001B[32;1m{str(text)}\u001B[0m"
-
-
=C2=A0def RED(text: str) -> str:
=C2=A0 =C2=A0 =C2=A0return f"\u001B[31;1m{str(text)}\u001B[0m"

-class EnvVarsDict(dict):
-=C2=A0 =C2=A0 def __str__(self) -> str:
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return " ".join(["=3D".joi= n(item) for item in self.items()])
-
-
=C2=A0class MesonArgs(object):
=C2=A0 =C2=A0 =C2=A0"""
=C2=A0 =C2=A0 =C2=A0Aggregate the arguments needed to build DPDK:
diff --git a/dts/poetry.lock b/dts/poetry.lock
index 0b2a007d4d..2438f337cd 100644
--- a/dts/poetry.lock
+++ b/dts/poetry.lock
@@ -12,6 +12,18 @@ docs =3D ["furo", "sphinx", "zo= pe.interface", "sphinx-notfound-page"]
=C2=A0tests =3D ["coverage[toml] (>=3D5.0.2)", "hypothesi= s", "pympler", "pytest (>=3D4.3.0)", "mypy= (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins", "zop= e.interface", "cloudpickle"]
=C2=A0tests_no_zope =3D ["coverage[toml] (>=3D5.0.2)", "h= ypothesis", "pympler", "pytest (>=3D4.3.0)", &q= uot;mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins", &= quot;cloudpickle"]

+[[package]]
+name =3D "bcrypt"
+version =3D "4.0.1"
+description =3D "Modern password hashing for your software and your s= ervers"
+category =3D "main"
+optional =3D false
+python-versions =3D ">=3D3.6"
+
+[package.extras]
+tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)"]
+typecheck =3D ["mypy"]
+
=C2=A0[[package]]
=C2=A0name =3D "black"
=C2=A0version =3D "22.10.0"
@@ -33,6 +45,17 @@ d =3D ["aiohttp (>=3D3.7.4)"]
=C2=A0jupyter =3D ["ipython (>=3D7.8.0)", "tokenize-rt (&= gt;=3D3.2.0)"]
=C2=A0uvloop =3D ["uvloop (>=3D0.15.2)"]

+[[package]]
+name =3D "cffi"
+version =3D "1.15.1"
+description =3D "Foreign Function Interface for Python calling C code= ."
+category =3D "main"
+optional =3D false
+python-versions =3D "*"
+
+[package.dependencies]
+pycparser =3D "*"
+
=C2=A0[[package]]
=C2=A0name =3D "click"
=C2=A0version =3D "8.1.3"
@@ -52,6 +75,52 @@ category =3D "dev"
=C2=A0optional =3D false
=C2=A0python-versions =3D "!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D3.3.*,!= =3D3.4.*,!=3D3.5.*,!=3D3.6.*,>=3D2.7"

+[[package]]
+name =3D "cryptography"
+version =3D "40.0.2"
+description =3D "cryptography is a package which provides cryptograph= ic recipes and primitives to Python developers."
+category =3D "main"
+optional =3D false
+python-versions =3D ">=3D3.6"
+
+[package.dependencies]
+cffi =3D ">=3D1.12"
+
+[package.extras]
+docs =3D ["sphinx (>=3D5.3.0)", "sphinx-rtd-theme (>= =3D1.1.1)"]
+docstest =3D ["pyenchant (>=3D1.6.11)", "twine (>=3D1= .12.0)", "sphinxcontrib-spelling (>=3D4.0.1)"]
+pep8test =3D ["black", "ruff", "mypy", "= ;check-manifest"]
+sdist =3D ["setuptools-rust (>=3D0.11.4)"]
+ssh =3D ["bcrypt (>=3D3.1.5)"]
+test =3D ["pytest (>=3D6.2.0)", "pytest-shard (>=3D0.= 1.2)", "pytest-benchmark", "pytest-cov", "pyt= est-subtests", "pytest-xdist", "pretend", "is= o8601"]
+test-randomorder =3D ["pytest-randomly"]
+tox =3D ["tox"]
+
+[[package]]
+name =3D "fabric"
+version =3D "2.7.1"
+description =3D "High level SSH command execution"
+category =3D "main"
+optional =3D false
+python-versions =3D "*"
+
+[package.dependencies]
+invoke =3D ">=3D1.3,<2.0"
+paramiko =3D ">=3D2.4"
+pathlib2 =3D "*"
+
+[package.extras]
+pytest =3D ["mock (>=3D2.0.0,<3.0)", "pytest (>=3D= 3.2.5,<4.0)"]
+testing =3D ["mock (>=3D2.0.0,<3.0)"]
+
+[[package]]
+name =3D "invoke"
+version =3D "1.7.3"
+description =3D "Pythonic task execution"
+category =3D "main"
+optional =3D false
+python-versions =3D "*"
+
=C2=A0[[package]]
=C2=A0name =3D "isort"
=C2=A0version =3D "5.10.1"
@@ -136,23 +205,41 @@ optional =3D false
=C2=A0python-versions =3D "*"

=C2=A0[[package]]
-name =3D "pathspec"
-version =3D "0.10.1"
-description =3D "Utility library for gitignore style pattern matching= of file paths."
-category =3D "dev"
+name =3D "paramiko"
+version =3D "3.1.0"
+description =3D "SSH2 protocol library"
+category =3D "main"
=C2=A0optional =3D false
-python-versions =3D ">=3D3.7"
+python-versions =3D ">=3D3.6"
+
+[package.dependencies]
+bcrypt =3D ">=3D3.2"
+cryptography =3D ">=3D3.3"
+pynacl =3D ">=3D1.5"
+
+[package.extras]
+all =3D ["pyasn1 (>=3D0.1.7)", "invoke (>=3D2.0)"= ;, "gssapi (>=3D1.4.1)", "pywin32 (>=3D2.1.8)"] +gssapi =3D ["pyasn1 (>=3D0.1.7)", "gssapi (>=3D1.4.1)= ", "pywin32 (>=3D2.1.8)"]
+invoke =3D ["invoke (>=3D2.0)"]

=C2=A0[[package]]
-name =3D "pexpect"
-version =3D "4.8.0"
-description =3D "Pexpect allows easy control of interactive console a= pplications."
+name =3D "pathlib2"
+version =3D "2.3.7.post1"
+description =3D "Object-oriented filesystem paths"
=C2=A0category =3D "main"
=C2=A0optional =3D false
=C2=A0python-versions =3D "*"

=C2=A0[package.dependencies]
-ptyprocess =3D ">=3D0.5"
+six =3D "*"
+
+[[package]]
+name =3D "pathspec"
+version =3D "0.10.1"
+description =3D "Utility library for gitignore style pattern matching= of file paths."
+category =3D "dev"
+optional =3D false
+python-versions =3D ">=3D3.7"

=C2=A0[[package]]
=C2=A0name =3D "platformdirs"
@@ -166,14 +253,6 @@ python-versions =3D ">=3D3.7"
=C2=A0docs =3D ["furo (>=3D2021.7.5b38)", "proselint (>= ;=3D0.10.2)", "sphinx-autodoc-typehints (>=3D1.12)", &quo= t;sphinx (>=3D4)"]
=C2=A0test =3D ["appdirs (=3D=3D1.4.4)", "pytest-cov (>= =3D2.7)", "pytest-mock (>=3D3.6)", "pytest (>=3D6= )"]

-[[package]]
-name =3D "ptyprocess"
-version =3D "0.7.0"
-description =3D "Run a subprocess in a pseudo terminal"
-category =3D "main"
-optional =3D false
-python-versions =3D "*"
-
=C2=A0[[package]]
=C2=A0name =3D "pycodestyle"
=C2=A0version =3D "2.9.1"
@@ -182,6 +261,14 @@ category =3D "dev"
=C2=A0optional =3D false
=C2=A0python-versions =3D ">=3D3.6"

+[[package]]
+name =3D "pycparser"
+version =3D "2.21"
+description =3D "C parser in Python"
+category =3D "main"
+optional =3D false
+python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*, != =3D3.3.*"
+
=C2=A0[[package]]
=C2=A0name =3D "pydocstyle"
=C2=A0version =3D "6.1.1"
@@ -228,6 +315,21 @@ tests =3D ["pytest (>=3D7.1.2)", "py= test-mypy", "eradicate (>=3D2.0.0)", "radon (>=3D= 5.1
=C2=A0toml =3D ["toml (>=3D0.10.2)"]
=C2=A0vulture =3D ["vulture"]

+[[package]]
+name =3D "pynacl"
+version =3D "1.5.0"
+description =3D "Python binding to the Networking and Cryptography (N= aCl) library"
+category =3D "main"
+optional =3D false
+python-versions =3D ">=3D3.6"
+
+[package.dependencies]
+cffi =3D ">=3D1.4.1"
+
+[package.extras]
+docs =3D ["sphinx (>=3D1.6.5)", "sphinx-rtd-theme"]=
+tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)", "hypothesis (= >=3D3.27.0)"]
+
=C2=A0[[package]]
=C2=A0name =3D "pyrsistent"
=C2=A0version =3D "0.19.1"
@@ -244,6 +346,14 @@ category =3D "main"
=C2=A0optional =3D false
=C2=A0python-versions =3D ">=3D3.6"

+[[package]]
+name =3D "six"
+version =3D "1.16.0"
+description =3D "Python 2 and 3 compatibility utilities"
+category =3D "main"
+optional =3D false
+python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*"= ;
+
=C2=A0[[package]]
=C2=A0name =3D "snowballstemmer"
=C2=A0version =3D "2.2.0"
@@ -299,13 +409,18 @@ jsonschema =3D ">=3D4,<5"
=C2=A0[metadata]
=C2=A0lock-version =3D "1.1"
=C2=A0python-versions =3D "^3.10"
-content-hash =3D "a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb9e260a67e= 92c2403e2319f"
+content-hash =3D "719c43bcaa5d181921debda884f8f714063df0b2336d61e9f64= ecab034e8b139"

=C2=A0[metadata.files]
=C2=A0attrs =3D []
+bcrypt =3D []
=C2=A0black =3D []
+cffi =3D []
=C2=A0click =3D []
=C2=A0colorama =3D []
+cryptography =3D []
+fabric =3D []
+invoke =3D []
=C2=A0isort =3D []
=C2=A0jsonpatch =3D []
=C2=A0jsonpointer =3D []
@@ -313,22 +428,22 @@ jsonschema =3D []
=C2=A0mccabe =3D []
=C2=A0mypy =3D []
=C2=A0mypy-extensions =3D []
+paramiko =3D []
+pathlib2 =3D []
=C2=A0pathspec =3D []
-pexpect =3D [
-=C2=A0 =C2=A0 {file =3D "pexpect-4.8.0-py2.py3-none-any.whl", ha= sh =3D "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69= ae3710937"},
-=C2=A0 =C2=A0 {file =3D "pexpect-4.8.0.tar.gz", hash =3D "s= ha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"= ;},
-]
=C2=A0platformdirs =3D [
=C2=A0 =C2=A0 =C2=A0{file =3D "platformdirs-2.5.2-py3-none-any.whl&quo= t;, hash =3D "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477= e722bb41ab25788"},
=C2=A0 =C2=A0 =C2=A0{file =3D "platformdirs-2.5.2.tar.gz", hash = =3D "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7= feef19"},
=C2=A0]
-ptyprocess =3D []
=C2=A0pycodestyle =3D []
+pycparser =3D []
=C2=A0pydocstyle =3D []
=C2=A0pyflakes =3D []
=C2=A0pylama =3D []
+pynacl =3D []
=C2=A0pyrsistent =3D []
=C2=A0pyyaml =3D []
+six =3D []
=C2=A0snowballstemmer =3D []
=C2=A0toml =3D []
=C2=A0tomli =3D []
diff --git a/dts/pyproject.toml b/dts/pyproject.toml
index a136c91e5e..50bcdb327a 100644
--- a/dts/pyproject.toml
+++ b/dts/pyproject.toml
@@ -9,10 +9,10 @@ authors =3D ["Owen Hilyard <ohilyard@iol.unh.edu>", &q= uot;dts@dpdk.org"= ;]

=C2=A0[tool.poetry.dependencies]
=C2=A0python =3D "^3.10"
-pexpect =3D "^4.8.0"
=C2=A0warlock =3D "^2.0.1"
=C2=A0PyYAML =3D "^6.0"
=C2=A0types-PyYAML =3D "^6.0.8"
+fabric =3D "^2.7.1"

=C2=A0[tool.poetry.dev-dependencies]
=C2=A0mypy =3D "^0.961"
--
2.34.1

--00000000000079a8d305fea80193--