From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <dev-bounces@dpdk.org>
Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124])
	by inbox.dpdk.org (Postfix) with ESMTP id 4389F42DDD;
	Wed,  5 Jul 2023 21:59:41 +0200 (CEST)
Received: from mails.dpdk.org (localhost [127.0.0.1])
	by mails.dpdk.org (Postfix) with ESMTP id 11017406B6;
	Wed,  5 Jul 2023 21:59:41 +0200 (CEST)
Received: from mail-pg1-f177.google.com (mail-pg1-f177.google.com
 [209.85.215.177])
 by mails.dpdk.org (Postfix) with ESMTP id B4DF3406B5
 for <dev@dpdk.org>; Wed,  5 Jul 2023 21:59:38 +0200 (CEST)
Received: by mail-pg1-f177.google.com with SMTP id
 41be03b00d2f7-5440e98616cso782844a12.0
 for <dev@dpdk.org>; Wed, 05 Jul 2023 12:59:38 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=iol.unh.edu; s=unh-iol; t=1688587178; x=1691179178;
 h=cc:to:subject:message-id:date:from:in-reply-to:references
 :mime-version:from:to:cc:subject:date:message-id:reply-to;
 bh=B1Z5Cgb/1ohGgyD//SyMdYf+CN0IhwnNyWtdv6cYVY0=;
 b=DNr1si8Gr8DiXwIwPnQYl9mfPIvq80otbYEw08to7Jz0suUMMbNahsKrPS0xZqI5VJ
 Ayhqy/dYCEiBNyODxfBNyjy47A0X5ewzqboSEejnzMUrIaGuOeo6+ldooJ0YHuNC1r0x
 Sq4L6GrhaSnnHXBUiS9rlgyFXRL6b5Nb1dHko=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20221208; t=1688587178; x=1691179178;
 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=B1Z5Cgb/1ohGgyD//SyMdYf+CN0IhwnNyWtdv6cYVY0=;
 b=GYiC6UIwYvcfAp6pXThLMa50kSDtBUfAoElS5meZlBRzoJvOTHsshIR18c3yKTRvXO
 R02vgKRxY37UtP8cokqzOF299utmOLFjWKJu9Jyu19+MJV8/C6n5ntr6bd9OmCrdicBJ
 BpJ/zOk5xO3w3WzMuKrfWUfgYB2Hn8XUnKIPoulPx/09g73vGmmtCrvSVt2rO0IEwv+/
 HQQHutaYjPOsoTpjfcuLlC3OssBiZFOpq0qNeLJAxQkULxKNDkUGwHpg5KfJGui/PmhW
 PMfk181+ES2WLEF7AUSyzgZqJGZQ3muwjdEVCxrbjiUnbRAI+Q1iAWwLsgwMNNZqOgBZ
 +vVg==
X-Gm-Message-State: ABy/qLarGAc1iKR2nzV7ziY4xt51EKDHK7eqlNzKG2pF8LlOjXZvX2uV
 X1PbI/OQFe3TV4CJfti2tqI6QJkY+QydQ8ohwi7lOw==
X-Google-Smtp-Source: APBJJlF5//DGI1+9MxDrPY4oJyiXGdj87rBVMD52zRC38jWO/QHmOcDJtqdnFThcqepPW9PbPSaN7YN9WT1zdNo3zmM=
X-Received: by 2002:a17:90a:778c:b0:262:eb20:2dd8 with SMTP id
 v12-20020a17090a778c00b00262eb202dd8mr73735pjk.20.1688587177715; Wed, 05 Jul
 2023 12:59:37 -0700 (PDT)
MIME-Version: 1.0
References: <20230424133537.58698-1-juraj.linkes@pantheon.tech>
 <20230609094640.130843-1-juraj.linkes@pantheon.tech>
 <CAAA20UQf+nxbpoNjUYsxBjJBBBqZZDMdXW7chhzmeunEoC5zzw@mail.gmail.com>
In-Reply-To: <CAAA20UQf+nxbpoNjUYsxBjJBBBqZZDMdXW7chhzmeunEoC5zzw@mail.gmail.com>
From: Jeremy Spewock <jspewock@iol.unh.edu>
Date: Wed, 5 Jul 2023 15:59:26 -0400
Message-ID: <CAAA20URr5Uv_RMhpuYcy-jB3kiNRivixDw25c3R6+_tnQ1S2iw@mail.gmail.com>
Subject: Re: [PATCH v3] dts: replace pexpect with fabric
To: =?UTF-8?Q?Juraj_Linke=C5=A1?= <juraj.linkes@pantheon.tech>
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="000000000000e67fc105ffc2d68e"
X-BeenThere: dev@dpdk.org
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: DPDK patches and discussions <dev.dpdk.org>
List-Unsubscribe: <https://mails.dpdk.org/options/dev>,
 <mailto:dev-request@dpdk.org?subject=unsubscribe>
List-Archive: <http://mails.dpdk.org/archives/dev/>
List-Post: <mailto:dev@dpdk.org>
List-Help: <mailto:dev-request@dpdk.org?subject=help>
List-Subscribe: <https://mails.dpdk.org/listinfo/dev>,
 <mailto:dev-request@dpdk.org?subject=subscribe>
Errors-To: dev-bounces@dpdk.org

--000000000000e67fc105ffc2d68e
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Tested-by: Jeremy Spewock <jspewock@iol.unh.edu>

On Wed, Jun 21, 2023 at 2:33=E2=80=AFPM Jeremy Spewock <jspewock@iol.unh.ed=
u> wrote:

> 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@pa=
ntheon.tech>
> 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 <juraj.linkes@pantheon.tech>
>> ---
>>
>> 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 th=
e
>> 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 passwordles=
s
>> 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 <sut_user>
>>
>>  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 <sut_admin_user>`
>> +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=3DTrue
>>          )
>> 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 alread=
y
>>          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 remo=
te
>> +        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
>> Node.
>>          """
>>
>>      @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_gue=
ss}
>> | 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_LIBDI=
R
>> -            out =3D self.remote_session.send_command("find /usr -type d
>> -name pkgconfig")
>> +            out =3D self.send_command("find /usr -type d -name pkgconfi=
g")
>>              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=3D=
force)
>> -        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 fro=
m
>> 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, "confi=
g")
>>              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
>> optional 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 remo=
te
>> +        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
>> the 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
>> Node.
>>          """
>> 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 fo=
r
>> .+:)"
>> -        )
>> -        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 Non=
e
>> +            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 t=
o
>> complete.
>> +            env: Extra environment variables that will be used in
>> command 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=
=3Dtimeout
>> +            )
>>
>> -        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 connecti=
ng"
>> -        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, NodeConfiguratio=
n
>>  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
>> config.
>>          """
>> -        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", "pyte=
st
>> (>=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
>> servers"
>> +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-benchmark", "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 o=
f
>> 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 o=
f
>> 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 (NaC=
l)
>> 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 <ohilyard@iol.unh.edu>", "
>> 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
>>
>>

--000000000000e67fc105ffc2d68e
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div style=3D"font-family:arial,sans-serif" class=3D"gmail=
_default">Tested-by: Jeremy Spewock &lt;<a href=3D"mailto:jspewock@iol.unh.=
edu" target=3D"_blank">jspewock@iol.unh.edu</a>&gt; </div></div><br><div cl=
ass=3D"gmail_quote"><div dir=3D"ltr" class=3D"gmail_attr">On Wed, Jun 21, 2=
023 at 2:33=E2=80=AFPM Jeremy Spewock &lt;<a href=3D"mailto:jspewock@iol.un=
h.edu">jspewock@iol.unh.edu</a>&gt; wrote:<br></div><blockquote class=3D"gm=
ail_quote" style=3D"margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,=
204,204);padding-left:1ex"><div dir=3D"ltr"><div class=3D"gmail_default" st=
yle=3D"font-family:arial,sans-serif">Acked-by: Jeremy Spewock &lt;<a href=
=3D"mailto:jspewock@iol.unh.edu" target=3D"_blank">jspewock@iol.unh.edu</a>=
&gt; <br></div></div><br><div class=3D"gmail_quote"><div dir=3D"ltr" class=
=3D"gmail_attr">On Fri, Jun 9, 2023 at 5:46=E2=80=AFAM Juraj Linke=C5=A1 &l=
t;juraj.linkes@pantheon.tech&gt; wrote:<br></div><blockquote class=3D"gmail=
_quote" style=3D"margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204=
,204);padding-left:1ex">Pexpect is not a dedicated SSH connection library w=
hile Fabric is. With<br>
Fabric, all SSH-related logic is provided and we can just focus on<br>
what&#39;s DTS specific.<br>
<br>
Signed-off-by: Juraj Linke=C5=A1 &lt;juraj.linkes@pantheon.tech&gt;<br>
---<br>
<br>
Notes:<br>
=C2=A0 =C2=A0 v3: updated passwordless sudo setup on Linux<br>
<br>
=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 +-<br>
=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 +-<br>
=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 +-<br>
=C2=A0dts/framework/remote_session/linux_session.py |=C2=A0 31 +-<br>
=C2=A0dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2=A0 51 ++=
+-<br>
=C2=A0dts/framework/remote_session/posix_session.py |=C2=A0 48 +--<br>
=C2=A0.../remote_session/remote/remote_session.py=C2=A0 =C2=A0|=C2=A0 35 ++=
-<br>
=C2=A0.../remote_session/remote/ssh_session.py=C2=A0 =C2=A0 =C2=A0 | 287 ++=
++++------------<br>
=C2=A0dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 =C2=A0|=
=C2=A0 12 +-<br>
=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 -<br>
=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 ++++++++--=
<br>
=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 +-<br>
=C2=A012 files changed, 376 insertions(+), 301 deletions(-)<br>
<br>
diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst<br>
index ebd6dceb6a..c7b31623e4 100644<br>
--- a/doc/guides/tools/dts.rst<br>
+++ b/doc/guides/tools/dts.rst<br>
@@ -95,9 +95,14 @@ Setting up DTS environment<br>
<br>
=C2=A0#. **SSH Connection**<br>
<br>
-=C2=A0 =C2=A0DTS uses Python pexpect for SSH connections between DTS envir=
onment and the other hosts.<br>
-=C2=A0 =C2=A0The pexpect implementation is a wrapper around the ssh comman=
d in the DTS environment.<br>
-=C2=A0 =C2=A0This means it&#39;ll use the SSH agent providing the ssh comm=
and and its keys.<br>
+=C2=A0 =C2=A0DTS uses the Fabric Python library for SSH connections betwee=
n DTS environment<br>
+=C2=A0 =C2=A0and the other hosts.<br>
+=C2=A0 =C2=A0The authentication method used is pubkey authentication.<br>
+=C2=A0 =C2=A0Fabric tries to use a passed key/certificate,<br>
+=C2=A0 =C2=A0then any key it can with through an SSH agent,<br>
+=C2=A0 =C2=A0then any &quot;id_rsa&quot;, &quot;id_dsa&quot; or &quot;id_e=
cdsa&quot; key discoverable in ``~/.ssh/``<br>
+=C2=A0 =C2=A0(with any matching OpenSSH-style certificates).<br>
+=C2=A0 =C2=A0DTS doesn&#39;t pass any keys, so Fabric tries to use the oth=
er two methods.<br>
<br>
<br>
=C2=A0Setting up System Under Test<br>
@@ -132,6 +137,21 @@ There are two areas that need to be set up on a System=
 Under Test:<br>
=C2=A0 =C2=A0 =C2=A0 It&#39;s possible to use the hugepage configuration al=
ready present on the SUT.<br>
=C2=A0 =C2=A0 =C2=A0 If you wish to do so, don&#39;t specify the hugepage c=
onfiguration in the DTS config file.<br>
<br>
+#. **User with administrator privileges**<br>
+<br>
+.. _sut_admin_user:<br>
+<br>
+=C2=A0 =C2=A0DTS needs administrator privileges to run DPDK applications (=
such as testpmd) on the SUT.<br>
+=C2=A0 =C2=A0The SUT user must be able run commands in privileged mode wit=
hout asking for password.<br>
+=C2=A0 =C2=A0On most Linux distributions, it&#39;s a matter of setting up =
passwordless sudo:<br>
+<br>
+=C2=A0 =C2=A0#. Run ``sudo visudo`` and check that it contains ``%sudo=C2=
=A0 =C2=A0ALL=3D(ALL:ALL) NOPASSWD:ALL``.<br>
+<br>
+=C2=A0 =C2=A0#. Add the SUT user to the sudo group with:<br>
+<br>
+=C2=A0 =C2=A0.. code-block:: console<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 sudo usermod -aG sudo &lt;sut_user&gt;<br>
<br>
=C2=A0Running DTS<br>
=C2=A0-----------<br>
@@ -151,7 +171,8 @@ which is a template that illustrates what can be config=
ured in DTS:<br>
=C2=A0 =C2=A0 =C2=A0 :start-at: executions:<br>
<br>
<br>
-The user must be root or any other user with prompt starting with ``#``.<b=
r>
+The user must have :ref:`administrator privileges &lt;sut_admin_user&gt;`<=
br>
+which don&#39;t require password authentication.<br>
=C2=A0The other fields are mostly self-explanatory<br>
=C2=A0and documented in more detail in ``dts/framework/config/conf_yaml_sch=
ema.json``.<br>
<br>
diff --git a/dts/conf.yaml b/dts/conf.yaml<br>
index a9bd8a3ecf..129801d87c 100644<br>
--- a/dts/conf.yaml<br>
+++ b/dts/conf.yaml<br>
@@ -16,7 +16,7 @@ executions:<br>
=C2=A0nodes:<br>
=C2=A0 =C2=A0- name: &quot;SUT 1&quot;<br>
=C2=A0 =C2=A0 =C2=A0hostname: sut1.change.me.localhost<br>
-=C2=A0 =C2=A0 user: root<br>
+=C2=A0 =C2=A0 user: dtsuser<br>
=C2=A0 =C2=A0 =C2=A0arch: x86_64<br>
=C2=A0 =C2=A0 =C2=A0os: linux<br>
=C2=A0 =C2=A0 =C2=A0lcores: &quot;&quot;<br>
diff --git a/dts/framework/exception.py b/dts/framework/exception.py<br>
index ca353d98fc..44ff4e979a 100644<br>
--- a/dts/framework/exception.py<br>
+++ b/dts/framework/exception.py<br>
@@ -62,13 +62,19 @@ class SSHConnectionError(DTSError):<br>
=C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
=C2=A0 =C2=A0 =C2=A0host: str<br>
+=C2=A0 =C2=A0 errors: list[str]<br>
=C2=A0 =C2=A0 =C2=A0severity: ClassVar[ErrorSeverity] =3D ErrorSeverity.SSH=
_ERR<br>
<br>
-=C2=A0 =C2=A0 def __init__(self, host: str):<br>
+=C2=A0 =C2=A0 def __init__(self, host: str, errors: list[str] | None =3D N=
one):<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.host =3D host<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.errors =3D [] if errors is None else erro=
rs<br>
<br>
=C2=A0 =C2=A0 =C2=A0def __str__(self) -&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f&quot;Error trying to connect with {se=
lf.host}&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 message =3D f&quot;Error trying to connect wit=
h {self.host}.&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.errors:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message +=3D f&quot; Errors enco=
untered while retrying: {&#39;, &#39;.join(self.errors)}&quot;<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return message<br>
<br>
<br>
=C2=A0class SSHSessionDeadError(DTSError):<br>
diff --git a/dts/framework/remote_session/linux_session.py b/dts/framework/=
remote_session/linux_session.py<br>
index a1e3bc3a92..f13f399121 100644<br>
--- a/dts/framework/remote_session/linux_session.py<br>
+++ b/dts/framework/remote_session/linux_session.py<br>
@@ -14,10 +14,11 @@ class LinuxSession(PosixSession):<br>
=C2=A0 =C2=A0 =C2=A0The implementation of non-Posix compliant parts of Linu=
x remote sessions.<br>
=C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
+=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&gt; str:<b=
r>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f&quot;sudo -- sh -c &#39;{command}&#39=
;&quot;<br>
+<br>
=C2=A0 =C2=A0 =C2=A0def get_remote_cpus(self, use_first_core: bool) -&gt; l=
ist[LogicalCore]:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.remote_session.send_command(=
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;lscpu -p=3DCPU,CORE,SOCKET=
,NODE|grep -v \\#&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.send_command(&quot;lscpu -p=
=3DCPU,CORE,SOCKET,NODE|grep -v \\#&quot;).stdout<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcores =3D []<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for cpu_line in cpu_info.splitlines():<br=
>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0lcore, core, socket, node =
=3D map(int, cpu_line.split(&quot;,&quot;))<br>
@@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int, force_f=
irst_numa: bool) -&gt; None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._mount_huge_pages()<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _get_hugepage_size(self) -&gt; int:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.remote_session.send_com=
mand(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;awk &#39;/Hugepagesiz=
e/ {print $2}&#39; /proc/meminfo&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return int(hugepage_size)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _get_hugepages_total(self) -&gt; int:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.remote_session.send_c=
ommand(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;awk &#39;/HugePages_T=
otal/ { print $2 }&#39; /proc/meminfo&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return int(hugepages_total)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _get_numa_nodes(self) -&gt; list[int]:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.remote_sessi=
on.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.send_command=
(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;cat /sy=
s/devices/system/node/online&quot;, verify=3DTrue<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0numa_range =3D expand_range=
(numa_count)<br>
@@ -70,14 +71,12 @@ def _get_numa_nodes(self) -&gt; list[int]:<br>
=C2=A0 =C2=A0 =C2=A0def _mount_huge_pages(self) -&gt; None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._<a href=3D"http://logger.info" rel=
=3D"noreferrer" target=3D"_blank">logger.info</a>(&quot;Re-mounting Hugepag=
es.&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hugapge_fs_cmd =3D &quot;awk &#39;/hugetl=
bfs/ { print $2 }&#39; /proc/mounts&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&quot;umount=
 $({hugapge_fs_cmd})&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(hu=
gapge_fs_cmd)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;umount $({hugapge_fs_=
cmd})&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(hugapge_fs_cmd)<b=
r>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if result.stdout =3D=3D &quot;&quot;:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_mount_path =3D &quot=
;/mnt/huge&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command=
(f&quot;mkdir -p {remote_mount_path}&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command=
(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;mount -t hu=
getlbfs nodev {remote_mount_path}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;mkdir -=
p {remote_mount_path}&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;mount -=
t hugetlbfs nodev {remote_mount_path}&quot;)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _supports_numa(self) -&gt; bool:<br>
=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<br>
@@ -94,14 +93,12 @@ def _configure_huge_pages(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if force_first_numa and self._supports_nu=
ma():<br>
=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=
(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;echo 0 | su=
do tee {hugepage_config_path}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;echo 0 =
| tee {hugepage_config_path}&quot;, privileged=3DTrue)<br>
=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&quot;/sys/d=
evices/system/node/node{self._numa_nodes[0]}/hugepages&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;/hugep=
ages-{size}kB/nr_hugepages&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;echo {amount} | sudo tee =
{hugepage_config_path}&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;echo {amount} | tee {huge=
page_config_path}&quot;, privileged=3DTrue<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
diff --git a/dts/framework/remote_session/os_session.py b/dts/framework/rem=
ote_session/os_session.py<br>
index 4c48ae2567..bfd70bd480 100644<br>
--- a/dts/framework/remote_session/os_session.py<br>
+++ b/dts/framework/remote_session/os_session.py<br>
@@ -10,7 +10,7 @@<br>
=C2=A0from framework.logger import DTSLOG<br>
=C2=A0from framework.settings import SETTINGS<br>
=C2=A0from framework.testbed_model import LogicalCore<br>
-from framework.utils import EnvVarsDict, MesonArgs<br>
+from framework.utils import MesonArgs<br>
<br>
=C2=A0from .remote import CommandResult, RemoteSession, create_remote_sessi=
on<br>
<br>
@@ -53,17 +53,32 @@ def is_alive(self) -&gt; bool:<br>
=C2=A0 =C2=A0 =C2=A0def send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0command: str,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged: bool =3D False,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0verify: bool =3D False,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; CommandResult:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0An all-purpose API in case the command to=
 be executed is already<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0OS-agnostic, such as when the path to the=
 executed command has been<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0constructed beforehand.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if privileged:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D self._get_privileged=
_command(command)<br>
+<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.remote_session.send_command(c=
ommand, timeout, verify, env)<br>
<br>
+=C2=A0 =C2=A0 @abstractmethod<br>
+=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&gt; str:<b=
r>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Modify the command so that i=
t executes with administrative privileges.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to modify.<=
br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The modified command that execut=
es with administrative privileges.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
+<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
=C2=A0 =C2=A0 =C2=A0def guess_dpdk_remote_dir(self, remote_dir) -&gt; PureP=
ath:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
@@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PurePath) -&gt=
; PurePath:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
-=C2=A0 =C2=A0 def copy_file(<br>
+=C2=A0 =C2=A0 def copy_from(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from the remote =
Node to the local filesystem.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node associat=
ed with this remote<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the local files=
ystem.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the rem=
ote Node.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire=
ctory path on the local filesystem.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
+<br>
+=C2=A0 =C2=A0 @abstractmethod<br>
+=C2=A0 =C2=A0 def copy_to(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
+=C2=A0 =C2=A0 ) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from local files=
ystem to the remote Node.<br>
+<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Copy source_file from local filesystem to=
 destination_file<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with the remote =
session.<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the directio=
n - copy source_file from the<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated remote Node to destination_file on =
local storage.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with this remote=
 session.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the loc=
al filesystem.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire=
ctory path on the remote Node.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
@@ -128,7 +161,7 @@ def extract_remote_tarball(<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
=C2=A0 =C2=A0 =C2=A0def build_dpdk(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0meson_args: MesonArgs,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_dir: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_build_dir: str | PurePath,<br=
>
diff --git a/dts/framework/remote_session/posix_session.py b/dts/framework/=
remote_session/posix_session.py<br>
index d38062e8d6..8ca0acb429 100644<br>
--- a/dts/framework/remote_session/posix_session.py<br>
+++ b/dts/framework/remote_session/posix_session.py<br>
@@ -9,7 +9,7 @@<br>
=C2=A0from framework.config import Architecture<br>
=C2=A0from framework.exception import DPDKBuildError, RemoteCommandExecutio=
nError<br>
=C2=A0from framework.settings import SETTINGS<br>
-from framework.utils import EnvVarsDict, MesonArgs<br>
+from framework.utils import MesonArgs<br>
<br>
=C2=A0from .os_session import OSSession<br>
<br>
@@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -&gt; str:<br>
<br>
=C2=A0 =C2=A0 =C2=A0def guess_dpdk_remote_dir(self, remote_dir) -&gt; PureP=
osixPath:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_guess =3D self.join_remote_path(re=
mote_dir, &quot;dpdk-*&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(f&=
quot;ls -d {remote_guess} | tail -1&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f&quot;ls -d {rem=
ote_guess} | tail -1&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return PurePosixPath(result.stdout)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def get_remote_tmp_dir(self) -&gt; PurePosixPath:<br>
@@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architecture) -&g=
t; dict:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0env_vars =3D {}<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if arch =3D=3D Architecture.i686:<br>
=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<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send=
_command(&quot;find /usr -type d -name pkgconfig&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(&quot;=
find /usr -type d -name pkgconfig&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0pkg_path =3D &quot;&quot;<b=
r>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0res_path =3D out.stdout.spl=
it(&quot;\r\n&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for cur_path in res_path:<b=
r>
@@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Architecture) -=
&gt; dict:<br>
=C2=A0 =C2=A0 =C2=A0def join_remote_path(self, *args: str | PurePath) -&gt;=
 PurePosixPath:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return PurePosixPath(*args)<br>
<br>
-=C2=A0 =C2=A0 def copy_file(<br>
+=C2=A0 =C2=A0 def copy_from(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_file(source_file, des=
tination_file, source_remote)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_from(source_file, des=
tination_file)<br>
+<br>
+=C2=A0 =C2=A0 def copy_to(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
+=C2=A0 =C2=A0 ) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_to(source_file, desti=
nation_file)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def remove_remote_dir(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
@@ -80,24 +86,24 @@ def remove_remote_dir(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0force: bool =3D True,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0opts =3D PosixSession.combine_short_optio=
ns(r=3Drecursive, f=3Dforce)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&quot;rm{opt=
s} {remote_dir_path}&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;rm{opts} {remote_dir_=
path}&quot;)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def extract_remote_tarball(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_tarball_path: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0expected_dir: str | PurePath | None =3D N=
one,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;tar xfm {remote_tarb=
all_path} &quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;-C {PurePosixPath(re=
mote_tarball_path).parent}&quot;,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A060,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if expected_dir:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command=
(f&quot;ls {expected_dir}&quot;, verify=3DTrue)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;ls {exp=
ected_dir}&quot;, verify=3DTrue)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def build_dpdk(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0meson_args: MesonArgs,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_dir: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0remote_dpdk_build_dir: str | PurePath,<br=
>
@@ -108,7 +114,7 @@ def build_dpdk(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if rebuild:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# reconfigure=
, then build<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._<a href=
=3D"http://logger.info" rel=3D"noreferrer" target=3D"_blank">logger.info</a=
>(&quot;Reconfiguring DPDK build.&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_sessio=
n.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(=
<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0f&quot;meson configure {meson_args} {remote_dpdk_build_dir}&quot;,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0timeout,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0verify=3DTrue,<br>
@@ -118,7 +124,7 @@ def build_dpdk(<br>
=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<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._<a href=
=3D"http://logger.info" rel=3D"noreferrer" target=3D"_blank">logger.info</a=
>(&quot;Configuring DPDK build from scratch.&quot;)<br>
=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)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_sessio=
n.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(=
<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0f&quot;meson setup &quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0f&quot;{meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}&quot;,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0timeout,<br>
@@ -127,14 +133,14 @@ def build_dpdk(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._<a href=3D"http://log=
ger.info" rel=3D"noreferrer" target=3D"_blank">logger.info</a>(&quot;Buildi=
ng DPDK.&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command=
(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;ninja =
-C {remote_dpdk_build_dir}&quot;, timeout, verify=3DTrue, env=3Denv_vars<br=
>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
=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&quot=
;DPDK build failed when doing &#39;{e.command}&#39;.&quot;)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def get_dpdk_version(self, build_dir: str | PurePath) -=
&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;cat {self.join_remot=
e_path(build_dir, &#39;VERSION&#39;)}&quot;, verify=3DTrue<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return out.stdout<br>
@@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iter=
able[str]) -&gt; None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# kill and cleanup only if =
DPDK is running<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0dpdk_pids =3D self._get_dpd=
k_pids(dpdk_runtime_dirs)<br>
=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&quot;kill -9 {dpdk_pid}&quot;, 20)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(=
f&quot;kill -9 {dpdk_pid}&quot;, 20)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._check_dpdk_hugepages(=
dpdk_runtime_dirs)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remove_dpdk_runtime_d=
irs(dpdk_runtime_dirs)<br>
<br>
@@ -168,7 +174,7 @@ def _list_remote_dirs(self, remote_path: str | PurePath=
) -&gt; list[str] | None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Return a list of directories of the remot=
e_dir.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0If remote_path doesn&#39;t exist, return =
None.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_command(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0f&quot;ls -l {remote_path} =
| awk &#39;/^d/ {{print $NF}}&#39;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0).stdout<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if &quot;No such file or directory&quot; =
in out:<br>
@@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[st=
r | PurePath]) -&gt; list[in<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for dpdk_runtime_dir in dpdk_runtime_dirs=
:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0dpdk_config_file =3D PurePo=
sixPath(dpdk_runtime_dir, &quot;config&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._remote_files_exist=
s(dpdk_config_file):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remot=
e_session.send_command(<br>
-=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}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_=
command(f&quot;lsof -Fp {dpdk_config_file}&quot;).stdout<br>
=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&quot; not in out:<br>
=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():<br>
=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)<br>
@@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[st=
r | PurePath]) -&gt; list[in<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return pids<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _remote_files_exists(self, remote_path: PurePath) -=
&gt; bool:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_command(f&=
quot;test -e {remote_path}&quot;)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f&quot;test -e {r=
emote_path}&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return not result.return_code<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _check_dpdk_hugepages(<br>
@@ -202,9 +206,7 @@ def _check_dpdk_hugepages(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0for dpdk_runtime_dir in dpdk_runtime_dirs=
:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0hugepage_info =3D PurePosix=
Path(dpdk_runtime_dir, &quot;hugepage_info&quot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if self._remote_files_exist=
s(hugepage_info):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remot=
e_session.send_command(<br>
-=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}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_=
command(f&quot;lsof -Fp {hugepage_info}&quot;).stdout<br>
=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&quot; not in out:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0self._logger.warning(&quot;Some DPDK processes did not free hugepages.&q=
uot;)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0self._logger.warning(&quot;*******************************************&q=
uot;)<br>
diff --git a/dts/framework/remote_session/remote/remote_session.py b/dts/fr=
amework/remote_session/remote/remote_session.py<br>
index 91dee3cb4f..0647d93de4 100644<br>
--- a/dts/framework/remote_session/remote/remote_session.py<br>
+++ b/dts/framework/remote_session/remote/remote_session.py<br>
@@ -11,7 +11,6 @@<br>
=C2=A0from framework.exception import RemoteCommandExecutionError<br>
=C2=A0from framework.logger import DTSLOG<br>
=C2=A0from framework.settings import SETTINGS<br>
-from framework.utils import EnvVarsDict<br>
<br>
<br>
=C2=A0@dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)<br>
@@ -89,7 +88,7 @@ def send_command(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0command: str,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0timeout: float =3D SETTINGS.timeout,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0verify: bool =3D False,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; CommandResult:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Send a command to the connected node usin=
g optional env vars<br>
@@ -114,7 +113,7 @@ def send_command(<br>
<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
=C2=A0 =C2=A0 =C2=A0def _send_command(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa=
rsDict | None<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: dict =
| None<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; CommandResult:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Use the underlying protocol to execute th=
e command using optional env vars<br>
@@ -141,15 +140,33 @@ def is_alive(self) -&gt; bool:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
=C2=A0 =C2=A0 =C2=A0@abstractmethod<br>
-=C2=A0 =C2=A0 def copy_file(<br>
+=C2=A0 =C2=A0 def copy_from(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from the remote =
Node to the local filesystem.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node associat=
ed with this remote<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the local files=
ystem.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the rem=
ote Node.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire=
ctory path on the local filesystem.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesystem to dest=
ination_file on the remote Node<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated with the remote session.<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the directio=
n - copy source_file from the<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated Node to destination_file on local f=
ilesystem.<br>
+<br>
+=C2=A0 =C2=A0 @abstractmethod<br>
+=C2=A0 =C2=A0 def copy_to(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
+=C2=A0 =C2=A0 ) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from local files=
ystem to the remote Node.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesystem to dest=
ination_file<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with this remote=
 session.<br>
+<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file on the loc=
al filesystem.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a file or dire=
ctory path on the remote Node.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
diff --git a/dts/framework/remote_session/remote/ssh_session.py b/dts/frame=
work/remote_session/remote/ssh_session.py<br>
index 42ff9498a2..8d127f1601 100644<br>
--- a/dts/framework/remote_session/remote/ssh_session.py<br>
+++ b/dts/framework/remote_session/remote/ssh_session.py<br>
@@ -1,29 +1,49 @@<br>
=C2=A0# SPDX-License-Identifier: BSD-3-Clause<br>
-# Copyright(c) 2010-2014 Intel Corporation<br>
-# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.<br>
-# Copyright(c) 2022-2023 University of New Hampshire<br>
+# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
<br>
-import time<br>
+import socket<br>
+import traceback<br>
=C2=A0from pathlib import PurePath<br>
<br>
-import pexpect=C2=A0 # type: ignore<br>
-from pexpect import pxssh=C2=A0 # type: ignore<br>
+from fabric import Connection=C2=A0 # type: ignore[import]<br>
+from invoke.exceptions import (=C2=A0 # type: ignore[import]<br>
+=C2=A0 =C2=A0 CommandTimedOut,<br>
+=C2=A0 =C2=A0 ThreadException,<br>
+=C2=A0 =C2=A0 UnexpectedExit,<br>
+)<br>
+from paramiko.ssh_exception import (=C2=A0 # type: ignore[import]<br>
+=C2=A0 =C2=A0 AuthenticationException,<br>
+=C2=A0 =C2=A0 BadHostKeyException,<br>
+=C2=A0 =C2=A0 NoValidConnectionsError,<br>
+=C2=A0 =C2=A0 SSHException,<br>
+)<br>
<br>
=C2=A0from framework.config import NodeConfiguration<br>
=C2=A0from framework.exception import SSHConnectionError, SSHSessionDeadErr=
or, SSHTimeoutError<br>
=C2=A0from framework.logger import DTSLOG<br>
-from framework.utils import GREEN, RED, EnvVarsDict<br>
<br>
=C2=A0from .remote_session import CommandResult, RemoteSession<br>
<br>
<br>
=C2=A0class SSHSession(RemoteSession):<br>
-=C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 Module for creating Pexpect SSH remote sessions.<br>
+=C2=A0 =C2=A0 &quot;&quot;&quot;A persistent SSH connection to a remote No=
de.<br>
+<br>
+=C2=A0 =C2=A0 The connection is implemented with the Fabric Python library=
.<br>
+<br>
+=C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The configuration of the Node to =
connect to.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: The name of the session.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: The logger used for logging.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 This should be passed from the p=
arent OSSession.<br>
+<br>
+=C2=A0 =C2=A0 Attributes:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 session: The underlying Fabric SSH connection.=
<br>
+<br>
+=C2=A0 =C2=A0 Raises:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHConnectionError: The connection cannot be e=
stablished.<br>
=C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
<br>
-=C2=A0 =C2=A0 session: pxssh.pxssh<br>
-=C2=A0 =C2=A0 magic_prompt: str<br>
+=C2=A0 =C2=A0 session: Connection<br>
<br>
=C2=A0 =C2=A0 =C2=A0def __init__(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
@@ -31,218 +51,91 @@ def __init__(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0session_name: str,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0logger: DTSLOG,<br>
=C2=A0 =C2=A0 =C2=A0):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.magic_prompt =3D &quot;MAGIC PROMPT&quot;=
<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(SSHSession, self).__init__(node_con=
fig, session_name, logger)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _connect(self) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Create connection to assigned node.<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 errors =3D []<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0retry_attempts =3D 10<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0login_timeout =3D 20 if self.port else 10=
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 password_regex =3D (<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 r&quot;(?i)(?:password:)|(?:pass=
phrase for key)|(?i)(password for .+:)&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry=
_attempts):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session =3D p=
xssh.pxssh(encoding=3D&quot;utf-8&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self=
.session.login(<br>
-=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,<br>
-=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,<br>
-=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,<br>
-=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&quot;[$#&gt;]&quot;,<br>
-=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,<br>
-=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,<br>
-=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,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br=
>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 brea=
k<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception a=
s e:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self=
._logger.warning(e)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time=
.sleep(2)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self=
._<a href=3D"http://logger.info" rel=3D"noreferrer" target=3D"_blank">logge=
r.info</a>(<br>
-=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&quot;Retrying connection: retry number {retry_attempt + 1}.&qu=
ot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br=
>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
-=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&quot;)<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect(&quot;stty -ech=
o&quot;, &quot;#&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect(&quot;stty colu=
mns 1000&quot;, &quot;#&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect(&quot;bind &#39=
;set enable-bracketed-paste off&#39;&quot;, &quot;#&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
-=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, &quot;port&quot=
;, None):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 suggestion =3D (<b=
r>
-=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 &quot;<br>
-=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&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry_attempts):<br=
>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session =3D C=
onnection(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self=
.ip,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 user=
=3Dself.username,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port=
=3Dself.port,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 conn=
ect_kwargs=3D{&quot;password&quot;: self.password},<br>
+=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,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"h=
ttp://logger.info" rel=3D"noreferrer" target=3D"_blank">logger.info</a>(GRE=
EN(suggestion))<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionError(self.ho=
stname)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.open(=
)<br>
<br>
-=C2=A0 =C2=A0 def send_expect(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, prompt: str, timeout: floa=
t =3D 15, verify: bool =3D False<br>
-=C2=A0 =C2=A0 ) -&gt; str | int:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret =3D self.send_expect_base(co=
mmand, prompt, timeout)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if verify:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret_status =3D sel=
f.send_expect_base(&quot;echo $?&quot;, prompt, timeout)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
-=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)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if r=
etval:<br>
-=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&quot;Command: {command} failure!&quot;)<br>
-=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)<br>
-=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<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else=
:<br>
-=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<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except ValueError:=
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 retu=
rn ret<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ret<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;Exception h=
appened in [{command}] and output is &quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;[{self._get=
_output()}]&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e<br>
-<br>
-=C2=A0 =C2=A0 def send_expect_base(self, command: str, prompt: str, timeou=
t: float) -&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 original_prompt =3D self.session.PROMPT<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D prompt<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prompt(command, timeout)<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D original_prompt<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
-<br>
-=C2=A0 =C2=A0 def _clean_session(self) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.magic_prompt<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.get_output(timeout=3D0.01)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.UNIQUE_PR=
OMPT<br>
-<br>
-=C2=A0 =C2=A0 def _send_line(self, command: str) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h=
ostname)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(command) =3D=3D 2 and command.startswit=
h(&quot;^&quot;):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendcontrol(command=
[1])<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendline(command)<b=
r>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (ValueError, BadHostKeyEx=
ception, AuthenticationException) as e:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.excep=
tion(e)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectio=
nError(self.hostname) from e<br>
<br>
-=C2=A0 =C2=A0 def _prompt(self, command: str, timeout: float) -&gt; None:<=
br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.session.prompt(timeout):<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(command, s=
elf._get_output()) from None<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (NoValidConnectionsError,=
 socket.error, SSHException) as e:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.debug=
(traceback.format_exc())<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.warni=
ng(e)<br>
<br>
-=C2=A0 =C2=A0 def get_output(self, timeout: float =3D 15) -&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get all output before timeout<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(timeout)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._flush()<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
+=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:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 erro=
rs.append(error)<br>
<br>
-=C2=A0 =C2=A0 def _get_output(self) -&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h=
ostname)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self.session.before.rsplit(&quot;\r=
\n&quot;, 1)[0]<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if before =3D=3D &quot;[PEXPECT]&quot;:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return &quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"h=
ttp://logger.info" rel=3D"noreferrer" target=3D"_blank">logger.info</a>(<br=
>
+=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}.&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
<br>
-=C2=A0 =C2=A0 def _flush(self) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Clear all session buffer<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.buffer =3D &quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.before =3D &quot;&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionError(self.ho=
stname, errors)<br>
<br>
=C2=A0 =C2=A0 =C2=A0def is_alive(self) -&gt; bool:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.isalive()<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.is_connected<br>
<br>
=C2=A0 =C2=A0 =C2=A0def _send_command(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa=
rsDict | None<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: dict =
| None<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; CommandResult:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self._send_command_get_output(comma=
nd, timeout, env)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return_code =3D int(self._send_command_get_out=
put(&quot;echo $?&quot;, timeout, None))<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Send a command and return th=
e result of the execution.<br>
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 # we&#39;re capturing only stdout<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(<a href=3D"http://self.na=
me" rel=3D"noreferrer" target=3D"_blank">self.name</a>, command, output, &q=
uot;&quot;, return_code)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to execute.=
<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Wait at most this many =
seconds for the execution to complete.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env: Extra environment variables=
 that will be used in command execution.<br>
<br>
-=C2=A0 =C2=A0 def _send_command_get_output(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, env: EnvVa=
rsDict | None<br>
-=C2=A0 =C2=A0 ) -&gt; str:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHSessionDeadError: The session=
 died while executing the command.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHTimeoutError: The command exe=
cution timed out.<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0try:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if env:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D f&quot=
;{env} {command}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.session.run(<br>
+=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<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
<br>
-=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<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(0.1)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (UnexpectedExit, ThreadException) as e:=
<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(e)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadError(self.h=
ostname) from e<br>
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return output<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 except CommandTimedOut as e:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(e)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(command, e=
.result.stderr) from e<br>
<br>
-=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if force is True:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.is_alive():<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.logou=
t()<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 <a href=3D"http://self.name" rel=
=3D"noreferrer" target=3D"_blank">self.name</a>, command, output.stdout, ou=
tput.stderr, output.return_code<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
<br>
-=C2=A0 =C2=A0 def copy_file(<br>
+=C2=A0 =C2=A0 def copy_from(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0source_file: str | PurePath,<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0destination_file: str | PurePath,<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
=C2=A0 =C2=A0 =C2=A0) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a local file to a remote host.<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if source_remote:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file =3D f&quot;{self.use=
rname}@{self.ip}:{source_file}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file =3D f&quot;{sel=
f.username}@{self.ip}:{destination_file}&quot;<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.get(str(destination_file), str(so=
urce_file))<br>
<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D &quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.port:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D f&quot; -P {self.port}&=
quot;<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D (<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;scp -v{port} -o NoHostAut=
henticationForLocalhost=3Dyes&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot; {source_file} {destinati=
on_file}&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._spawn_scp(command)<br>
+=C2=A0 =C2=A0 def copy_to(<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
+=C2=A0 =C2=A0 ) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.put(str(source_file), str(destina=
tion_file))<br>
<br>
-=C2=A0 =C2=A0 def _spawn_scp(self, scp_cmd: str) -&gt; None:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 Transfer a file with SCP<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"http://logger.info" rel=3D"no=
referrer" target=3D"_blank">logger.info</a>(scp_cmd)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 p: pexpect.spawn =3D pexpect.spawn(scp_cmd)<br=
>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 ssh_newkey: str =3D &quot;Are you sure you wan=
t to continue connecting&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 i: int =3D p.expect(<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [ssh_newkey, &quot;[pP]assword&q=
uot;, &quot;# &quot;, pexpect.EOF, pexpect.TIMEOUT], 120<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
-=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(&quot;yes&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 i =3D p.expect([ssh_newkey, &quo=
t;[pP]assword&quot;, pexpect.EOF], 2)<br>
-<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 1:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline(self.password)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.expect(&quot;Exit status 0&quo=
t;, 60)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 4:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(&quot;SCP TIM=
EOUT error %d&quot; % i)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 p.close()<br>
+=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -&gt; None:<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()<br>
diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framework/testbe=
d_model/sut_node.py<br>
index 2b2b50d982..9dbc390848 100644<br>
--- a/dts/framework/testbed_model/sut_node.py<br>
+++ b/dts/framework/testbed_model/sut_node.py<br>
@@ -10,7 +10,7 @@<br>
=C2=A0from framework.config import BuildTargetConfiguration, NodeConfigurat=
ion<br>
=C2=A0from framework.remote_session import CommandResult, OSSession<br>
=C2=A0from framework.settings import SETTINGS<br>
-from framework.utils import EnvVarsDict, MesonArgs<br>
+from framework.utils import MesonArgs<br>
<br>
=C2=A0from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice<br>
=C2=A0from .node import Node<br>
@@ -27,7 +27,7 @@ class SutNode(Node):<br>
=C2=A0 =C2=A0 =C2=A0_dpdk_prefix_list: list[str]<br>
=C2=A0 =C2=A0 =C2=A0_dpdk_timestamp: str<br>
=C2=A0 =C2=A0 =C2=A0_build_target_config: BuildTargetConfiguration | None<b=
r>
-=C2=A0 =C2=A0 _env_vars: EnvVarsDict<br>
+=C2=A0 =C2=A0 _env_vars: dict<br>
=C2=A0 =C2=A0 =C2=A0_remote_tmp_dir: PurePath<br>
=C2=A0 =C2=A0 =C2=A0__remote_dpdk_dir: PurePath | None<br>
=C2=A0 =C2=A0 =C2=A0_dpdk_version: str | None<br>
@@ -38,7 +38,7 @@ def __init__(self, node_config: NodeConfiguration):<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0super(SutNode, self).__init__(node_config=
)<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._dpdk_prefix_list =3D []<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D None<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._remote_tmp_dir =3D self.main_sessio=
n.get_remote_tmp_dir()<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self.__remote_dpdk_dir =3D None<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._dpdk_version =3D None<br>
@@ -94,7 +94,7 @@ def _configure_build_target(<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Populate common environment variables and=
 set build target config.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._build_target_config =3D build_targe=
t_config<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._env_vars.update(<br>
=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)<br>
@@ -112,7 +112,7 @@ def _copy_dpdk_tarball(self) -&gt; None:<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0Copy to and extract DPDK tarball on the S=
UT node.<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0self._<a href=3D"http://logger.info" rel=
=3D"noreferrer" target=3D"_blank">logger.info</a>(&quot;Copying DPDK tarbal=
l to SUT.&quot;)<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_file(SETTINGS.dpdk_tarb=
all_path, self._remote_tmp_dir)<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_to(SETTINGS.dpdk_tarbal=
l_path, self._remote_tmp_dir)<br>
<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# construct remote tarball path<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0# the basename is the same on local host =
and on remote Node<br>
@@ -259,7 +259,7 @@ def run_dpdk_app(<br>
=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&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return self.main_session.send_command(<br=
>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;{app_path} {eal_args}&quo=
t;, timeout, verify=3DTrue<br>
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;{app_path} {eal_args}&quo=
t;, timeout, privileged=3DTrue, verify=3DTrue<br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0)<br>
<br>
<br>
diff --git a/dts/framework/utils.py b/dts/framework/utils.py<br>
index 55e0b0ef0e..8cfbc6a29d 100644<br>
--- a/dts/framework/utils.py<br>
+++ b/dts/framework/utils.py<br>
@@ -42,19 +42,10 @@ def expand_range(range_str: str) -&gt; list[int]:<br>
=C2=A0 =C2=A0 =C2=A0return expanded_range<br>
<br>
<br>
-def GREEN(text: str) -&gt; str:<br>
-=C2=A0 =C2=A0 return f&quot;\u001B[32;1m{str(text)}\u001B[0m&quot;<br>
-<br>
-<br>
=C2=A0def RED(text: str) -&gt; str:<br>
=C2=A0 =C2=A0 =C2=A0return f&quot;\u001B[31;1m{str(text)}\u001B[0m&quot;<br=
>
<br>
<br>
-class EnvVarsDict(dict):<br>
-=C2=A0 =C2=A0 def __str__(self) -&gt; str:<br>
-=C2=A0 =C2=A0 =C2=A0 =C2=A0 return &quot; &quot;.join([&quot;=3D&quot;.joi=
n(item) for item in self.items()])<br>
-<br>
-<br>
=C2=A0class MesonArgs(object):<br>
=C2=A0 =C2=A0 =C2=A0&quot;&quot;&quot;<br>
=C2=A0 =C2=A0 =C2=A0Aggregate the arguments needed to build DPDK:<br>
diff --git a/dts/poetry.lock b/dts/poetry.lock<br>
index 0b2a007d4d..2438f337cd 100644<br>
--- a/dts/poetry.lock<br>
+++ b/dts/poetry.lock<br>
@@ -12,6 +12,18 @@ docs =3D [&quot;furo&quot;, &quot;sphinx&quot;, &quot;zo=
pe.interface&quot;, &quot;sphinx-notfound-page&quot;]<br>
=C2=A0tests =3D [&quot;coverage[toml] (&gt;=3D5.0.2)&quot;, &quot;hypothesi=
s&quot;, &quot;pympler&quot;, &quot;pytest (&gt;=3D4.3.0)&quot;, &quot;mypy=
 (&gt;=3D0.900,!=3D0.940)&quot;, &quot;pytest-mypy-plugins&quot;, &quot;zop=
e.interface&quot;, &quot;cloudpickle&quot;]<br>
=C2=A0tests_no_zope =3D [&quot;coverage[toml] (&gt;=3D5.0.2)&quot;, &quot;h=
ypothesis&quot;, &quot;pympler&quot;, &quot;pytest (&gt;=3D4.3.0)&quot;, &q=
uot;mypy (&gt;=3D0.900,!=3D0.940)&quot;, &quot;pytest-mypy-plugins&quot;, &=
quot;cloudpickle&quot;]<br>
<br>
+[[package]]<br>
+name =3D &quot;bcrypt&quot;<br>
+version =3D &quot;4.0.1&quot;<br>
+description =3D &quot;Modern password hashing for your software and your s=
ervers&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D3.6&quot;<br>
+<br>
+[package.extras]<br>
+tests =3D [&quot;pytest (&gt;=3D3.2.1,!=3D3.3.0)&quot;]<br>
+typecheck =3D [&quot;mypy&quot;]<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;black&quot;<br>
=C2=A0version =3D &quot;22.10.0&quot;<br>
@@ -33,6 +45,17 @@ d =3D [&quot;aiohttp (&gt;=3D3.7.4)&quot;]<br>
=C2=A0jupyter =3D [&quot;ipython (&gt;=3D7.8.0)&quot;, &quot;tokenize-rt (&=
gt;=3D3.2.0)&quot;]<br>
=C2=A0uvloop =3D [&quot;uvloop (&gt;=3D0.15.2)&quot;]<br>
<br>
+[[package]]<br>
+name =3D &quot;cffi&quot;<br>
+version =3D &quot;1.15.1&quot;<br>
+description =3D &quot;Foreign Function Interface for Python calling C code=
.&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;*&quot;<br>
+<br>
+[package.dependencies]<br>
+pycparser =3D &quot;*&quot;<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;click&quot;<br>
=C2=A0version =3D &quot;8.1.3&quot;<br>
@@ -52,6 +75,52 @@ category =3D &quot;dev&quot;<br>
=C2=A0optional =3D false<br>
=C2=A0python-versions =3D &quot;!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D3.3.*,!=
=3D3.4.*,!=3D3.5.*,!=3D3.6.*,&gt;=3D2.7&quot;<br>
<br>
+[[package]]<br>
+name =3D &quot;cryptography&quot;<br>
+version =3D &quot;40.0.2&quot;<br>
+description =3D &quot;cryptography is a package which provides cryptograph=
ic recipes and primitives to Python developers.&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D3.6&quot;<br>
+<br>
+[package.dependencies]<br>
+cffi =3D &quot;&gt;=3D1.12&quot;<br>
+<br>
+[package.extras]<br>
+docs =3D [&quot;sphinx (&gt;=3D5.3.0)&quot;, &quot;sphinx-rtd-theme (&gt;=
=3D1.1.1)&quot;]<br>
+docstest =3D [&quot;pyenchant (&gt;=3D1.6.11)&quot;, &quot;twine (&gt;=3D1=
.12.0)&quot;, &quot;sphinxcontrib-spelling (&gt;=3D4.0.1)&quot;]<br>
+pep8test =3D [&quot;black&quot;, &quot;ruff&quot;, &quot;mypy&quot;, &quot=
;check-manifest&quot;]<br>
+sdist =3D [&quot;setuptools-rust (&gt;=3D0.11.4)&quot;]<br>
+ssh =3D [&quot;bcrypt (&gt;=3D3.1.5)&quot;]<br>
+test =3D [&quot;pytest (&gt;=3D6.2.0)&quot;, &quot;pytest-shard (&gt;=3D0.=
1.2)&quot;, &quot;pytest-benchmark&quot;, &quot;pytest-cov&quot;, &quot;pyt=
est-subtests&quot;, &quot;pytest-xdist&quot;, &quot;pretend&quot;, &quot;is=
o8601&quot;]<br>
+test-randomorder =3D [&quot;pytest-randomly&quot;]<br>
+tox =3D [&quot;tox&quot;]<br>
+<br>
+[[package]]<br>
+name =3D &quot;fabric&quot;<br>
+version =3D &quot;2.7.1&quot;<br>
+description =3D &quot;High level SSH command execution&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;*&quot;<br>
+<br>
+[package.dependencies]<br>
+invoke =3D &quot;&gt;=3D1.3,&lt;2.0&quot;<br>
+paramiko =3D &quot;&gt;=3D2.4&quot;<br>
+pathlib2 =3D &quot;*&quot;<br>
+<br>
+[package.extras]<br>
+pytest =3D [&quot;mock (&gt;=3D2.0.0,&lt;3.0)&quot;, &quot;pytest (&gt;=3D=
3.2.5,&lt;4.0)&quot;]<br>
+testing =3D [&quot;mock (&gt;=3D2.0.0,&lt;3.0)&quot;]<br>
+<br>
+[[package]]<br>
+name =3D &quot;invoke&quot;<br>
+version =3D &quot;1.7.3&quot;<br>
+description =3D &quot;Pythonic task execution&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;*&quot;<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;isort&quot;<br>
=C2=A0version =3D &quot;5.10.1&quot;<br>
@@ -136,23 +205,41 @@ optional =3D false<br>
=C2=A0python-versions =3D &quot;*&quot;<br>
<br>
=C2=A0[[package]]<br>
-name =3D &quot;pathspec&quot;<br>
-version =3D &quot;0.10.1&quot;<br>
-description =3D &quot;Utility library for gitignore style pattern matching=
 of file paths.&quot;<br>
-category =3D &quot;dev&quot;<br>
+name =3D &quot;paramiko&quot;<br>
+version =3D &quot;3.1.0&quot;<br>
+description =3D &quot;SSH2 protocol library&quot;<br>
+category =3D &quot;main&quot;<br>
=C2=A0optional =3D false<br>
-python-versions =3D &quot;&gt;=3D3.7&quot;<br>
+python-versions =3D &quot;&gt;=3D3.6&quot;<br>
+<br>
+[package.dependencies]<br>
+bcrypt =3D &quot;&gt;=3D3.2&quot;<br>
+cryptography =3D &quot;&gt;=3D3.3&quot;<br>
+pynacl =3D &quot;&gt;=3D1.5&quot;<br>
+<br>
+[package.extras]<br>
+all =3D [&quot;pyasn1 (&gt;=3D0.1.7)&quot;, &quot;invoke (&gt;=3D2.0)&quot=
;, &quot;gssapi (&gt;=3D1.4.1)&quot;, &quot;pywin32 (&gt;=3D2.1.8)&quot;]<b=
r>
+gssapi =3D [&quot;pyasn1 (&gt;=3D0.1.7)&quot;, &quot;gssapi (&gt;=3D1.4.1)=
&quot;, &quot;pywin32 (&gt;=3D2.1.8)&quot;]<br>
+invoke =3D [&quot;invoke (&gt;=3D2.0)&quot;]<br>
<br>
=C2=A0[[package]]<br>
-name =3D &quot;pexpect&quot;<br>
-version =3D &quot;4.8.0&quot;<br>
-description =3D &quot;Pexpect allows easy control of interactive console a=
pplications.&quot;<br>
+name =3D &quot;pathlib2&quot;<br>
+version =3D &quot;2.3.7.post1&quot;<br>
+description =3D &quot;Object-oriented filesystem paths&quot;<br>
=C2=A0category =3D &quot;main&quot;<br>
=C2=A0optional =3D false<br>
=C2=A0python-versions =3D &quot;*&quot;<br>
<br>
=C2=A0[package.dependencies]<br>
-ptyprocess =3D &quot;&gt;=3D0.5&quot;<br>
+six =3D &quot;*&quot;<br>
+<br>
+[[package]]<br>
+name =3D &quot;pathspec&quot;<br>
+version =3D &quot;0.10.1&quot;<br>
+description =3D &quot;Utility library for gitignore style pattern matching=
 of file paths.&quot;<br>
+category =3D &quot;dev&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D3.7&quot;<br>
<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;platformdirs&quot;<br>
@@ -166,14 +253,6 @@ python-versions =3D &quot;&gt;=3D3.7&quot;<br>
=C2=A0docs =3D [&quot;furo (&gt;=3D2021.7.5b38)&quot;, &quot;proselint (&gt=
;=3D0.10.2)&quot;, &quot;sphinx-autodoc-typehints (&gt;=3D1.12)&quot;, &quo=
t;sphinx (&gt;=3D4)&quot;]<br>
=C2=A0test =3D [&quot;appdirs (=3D=3D1.4.4)&quot;, &quot;pytest-cov (&gt;=
=3D2.7)&quot;, &quot;pytest-mock (&gt;=3D3.6)&quot;, &quot;pytest (&gt;=3D6=
)&quot;]<br>
<br>
-[[package]]<br>
-name =3D &quot;ptyprocess&quot;<br>
-version =3D &quot;0.7.0&quot;<br>
-description =3D &quot;Run a subprocess in a pseudo terminal&quot;<br>
-category =3D &quot;main&quot;<br>
-optional =3D false<br>
-python-versions =3D &quot;*&quot;<br>
-<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;pycodestyle&quot;<br>
=C2=A0version =3D &quot;2.9.1&quot;<br>
@@ -182,6 +261,14 @@ category =3D &quot;dev&quot;<br>
=C2=A0optional =3D false<br>
=C2=A0python-versions =3D &quot;&gt;=3D3.6&quot;<br>
<br>
+[[package]]<br>
+name =3D &quot;pycparser&quot;<br>
+version =3D &quot;2.21&quot;<br>
+description =3D &quot;C parser in Python&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*, !=
=3D3.3.*&quot;<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;pydocstyle&quot;<br>
=C2=A0version =3D &quot;6.1.1&quot;<br>
@@ -228,6 +315,21 @@ tests =3D [&quot;pytest (&gt;=3D7.1.2)&quot;, &quot;py=
test-mypy&quot;, &quot;eradicate (&gt;=3D2.0.0)&quot;, &quot;radon (&gt;=3D=
5.1<br>
=C2=A0toml =3D [&quot;toml (&gt;=3D0.10.2)&quot;]<br>
=C2=A0vulture =3D [&quot;vulture&quot;]<br>
<br>
+[[package]]<br>
+name =3D &quot;pynacl&quot;<br>
+version =3D &quot;1.5.0&quot;<br>
+description =3D &quot;Python binding to the Networking and Cryptography (N=
aCl) library&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D3.6&quot;<br>
+<br>
+[package.dependencies]<br>
+cffi =3D &quot;&gt;=3D1.4.1&quot;<br>
+<br>
+[package.extras]<br>
+docs =3D [&quot;sphinx (&gt;=3D1.6.5)&quot;, &quot;sphinx-rtd-theme&quot;]=
<br>
+tests =3D [&quot;pytest (&gt;=3D3.2.1,!=3D3.3.0)&quot;, &quot;hypothesis (=
&gt;=3D3.27.0)&quot;]<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;pyrsistent&quot;<br>
=C2=A0version =3D &quot;0.19.1&quot;<br>
@@ -244,6 +346,14 @@ category =3D &quot;main&quot;<br>
=C2=A0optional =3D false<br>
=C2=A0python-versions =3D &quot;&gt;=3D3.6&quot;<br>
<br>
+[[package]]<br>
+name =3D &quot;six&quot;<br>
+version =3D &quot;1.16.0&quot;<br>
+description =3D &quot;Python 2 and 3 compatibility utilities&quot;<br>
+category =3D &quot;main&quot;<br>
+optional =3D false<br>
+python-versions =3D &quot;&gt;=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*&quot=
;<br>
+<br>
=C2=A0[[package]]<br>
=C2=A0name =3D &quot;snowballstemmer&quot;<br>
=C2=A0version =3D &quot;2.2.0&quot;<br>
@@ -299,13 +409,18 @@ jsonschema =3D &quot;&gt;=3D4,&lt;5&quot;<br>
=C2=A0[metadata]<br>
=C2=A0lock-version =3D &quot;1.1&quot;<br>
=C2=A0python-versions =3D &quot;^3.10&quot;<br>
-content-hash =3D &quot;a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb9e260a67e=
92c2403e2319f&quot;<br>
+content-hash =3D &quot;719c43bcaa5d181921debda884f8f714063df0b2336d61e9f64=
ecab034e8b139&quot;<br>
<br>
=C2=A0[metadata.files]<br>
=C2=A0attrs =3D []<br>
+bcrypt =3D []<br>
=C2=A0black =3D []<br>
+cffi =3D []<br>
=C2=A0click =3D []<br>
=C2=A0colorama =3D []<br>
+cryptography =3D []<br>
+fabric =3D []<br>
+invoke =3D []<br>
=C2=A0isort =3D []<br>
=C2=A0jsonpatch =3D []<br>
=C2=A0jsonpointer =3D []<br>
@@ -313,22 +428,22 @@ jsonschema =3D []<br>
=C2=A0mccabe =3D []<br>
=C2=A0mypy =3D []<br>
=C2=A0mypy-extensions =3D []<br>
+paramiko =3D []<br>
+pathlib2 =3D []<br>
=C2=A0pathspec =3D []<br>
-pexpect =3D [<br>
-=C2=A0 =C2=A0 {file =3D &quot;pexpect-4.8.0-py2.py3-none-any.whl&quot;, ha=
sh =3D &quot;sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69=
ae3710937&quot;},<br>
-=C2=A0 =C2=A0 {file =3D &quot;pexpect-4.8.0.tar.gz&quot;, hash =3D &quot;s=
ha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c&quot=
;},<br>
-]<br>
=C2=A0platformdirs =3D [<br>
=C2=A0 =C2=A0 =C2=A0{file =3D &quot;platformdirs-2.5.2-py3-none-any.whl&quo=
t;, hash =3D &quot;sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477=
e722bb41ab25788&quot;},<br>
=C2=A0 =C2=A0 =C2=A0{file =3D &quot;platformdirs-2.5.2.tar.gz&quot;, hash =
=3D &quot;sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7=
feef19&quot;},<br>
=C2=A0]<br>
-ptyprocess =3D []<br>
=C2=A0pycodestyle =3D []<br>
+pycparser =3D []<br>
=C2=A0pydocstyle =3D []<br>
=C2=A0pyflakes =3D []<br>
=C2=A0pylama =3D []<br>
+pynacl =3D []<br>
=C2=A0pyrsistent =3D []<br>
=C2=A0pyyaml =3D []<br>
+six =3D []<br>
=C2=A0snowballstemmer =3D []<br>
=C2=A0toml =3D []<br>
=C2=A0tomli =3D []<br>
diff --git a/dts/pyproject.toml b/dts/pyproject.toml<br>
index a136c91e5e..50bcdb327a 100644<br>
--- a/dts/pyproject.toml<br>
+++ b/dts/pyproject.toml<br>
@@ -9,10 +9,10 @@ authors =3D [&quot;Owen Hilyard &lt;<a href=3D"mailto:ohi=
lyard@iol.unh.edu" target=3D"_blank">ohilyard@iol.unh.edu</a>&gt;&quot;, &q=
uot;<a href=3D"mailto:dts@dpdk.org" target=3D"_blank">dts@dpdk.org</a>&quot=
;]<br>
<br>
=C2=A0[tool.poetry.dependencies]<br>
=C2=A0python =3D &quot;^3.10&quot;<br>
-pexpect =3D &quot;^4.8.0&quot;<br>
=C2=A0warlock =3D &quot;^2.0.1&quot;<br>
=C2=A0PyYAML =3D &quot;^6.0&quot;<br>
=C2=A0types-PyYAML =3D &quot;^6.0.8&quot;<br>
+fabric =3D &quot;^2.7.1&quot;<br>
<br>
=C2=A0[tool.poetry.dev-dependencies]<br>
=C2=A0mypy =3D &quot;^0.961&quot;<br>
-- <br>
2.34.1<br>
<br>
</blockquote></div>
</blockquote></div>

--000000000000e67fc105ffc2d68e--