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 37CB142A51;
	Wed,  3 May 2023 19:54:20 +0200 (CEST)
Received: from mails.dpdk.org (localhost [127.0.0.1])
	by mails.dpdk.org (Postfix) with ESMTP id 0F76E41144;
	Wed,  3 May 2023 19:54:20 +0200 (CEST)
Received: from mail-pj1-f50.google.com (mail-pj1-f50.google.com
 [209.85.216.50]) by mails.dpdk.org (Postfix) with ESMTP id 9C34E410F9
 for <dev@dpdk.org>; Wed,  3 May 2023 19:54:17 +0200 (CEST)
Received: by mail-pj1-f50.google.com with SMTP id
 98e67ed59e1d1-2496863c2c7so5284050a91.1
 for <dev@dpdk.org>; Wed, 03 May 2023 10:54:17 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=iol.unh.edu; s=unh-iol; t=1683136457; x=1685728457;
 h=cc:to:subject:message-id:date:from:in-reply-to:references
 :mime-version:from:to:cc:subject:date:message-id:reply-to;
 bh=V5ARh2NV8/LaYiIFJwYBk9SNW4LOiZy1cLEX9j6XTTk=;
 b=NB7dSi0HqHvrC05PkjGqWptsHq2dwSZs2ZcM5zs8erqmWxLfNdQaouSQf1WaBUMQPM
 6LtcGusGXtREQB1WIiYJ6yF7jCDqZHh0eIt1PjJ/1GuKAh+1OfNlmfNR9il9+TGLuv5k
 7a3xRDtQAx/B4Xtb2AW/cU8bZFVHyEGXlF/ko=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=1e100.net; s=20221208; t=1683136457; x=1685728457;
 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=V5ARh2NV8/LaYiIFJwYBk9SNW4LOiZy1cLEX9j6XTTk=;
 b=IGvkm2ok9oJo1r+AZAKs/lr6xn6FzgtjwUngtzdFOzYBYRT4KNTEH1mOYpSea+J027
 ORoZYtNoE0RNy1VJIMFMKFEzg+Mtaxj0YpWMoXSGz/Oy4qpu+O3eWVM6G5bkAxH+6WiN
 TRsn4zMZgMe1ji4gOCNBwsqymg2UdNyeI7Dxw8oRjABp8uNGD8AQhqiOdzN1Gux4up+g
 5xURS50wEU8yaAeZZGIKeEKC/XUlYItPqIYRSkkmAhjFarCW9Q/5UvocwO8nSm8SMEdV
 zePiiZYWr0T/dpAPKWQoQxn2DDu6fxGt9qaXXFnvCHUz+6XMrNxwehC7xr0J0bSD0S3V
 Yicw==
X-Gm-Message-State: AC+VfDwRDM4DGY+6x8/HskLT0xRCMlF80y0GLNzvcEZNgPzHzt/2IpMS
 0xKvHPEhFYBdF8EYZ5NJ1L8dHz+wnA2t5pHOb1ka1w==
X-Google-Smtp-Source: ACHHUZ7L3zLrKlpRGSfb2eaJeb5WaVzjbVA5yjJyx9JGDNzporBImOPfILlXJyyjaLwP5NikuEXECH9oo5EApdBav+0=
X-Received: by 2002:a17:90a:ea92:b0:24d:fee5:6e3e with SMTP id
 h18-20020a17090aea9200b0024dfee56e3emr10369194pjz.25.1683136456342; Wed, 03
 May 2023 10:54:16 -0700 (PDT)
MIME-Version: 1.0
References: <20230403114608.1423020-2-juraj.linkes@pantheon.tech>
 <20230424133537.58698-1-juraj.linkes@pantheon.tech>
 <CAAA20UTQakV6nNLEvm6HkDWEBx7UUcT1AYNegN5hokzebqM9=g@mail.gmail.com>
 <CAOb5WZYrTC2KCoykVZSVS3ppMRzmnomKzrYbZ+cLryMxaRJ-2Q@mail.gmail.com>
In-Reply-To: <CAOb5WZYrTC2KCoykVZSVS3ppMRzmnomKzrYbZ+cLryMxaRJ-2Q@mail.gmail.com>
From: Jeremy Spewock <jspewock@iol.unh.edu>
Date: Wed, 3 May 2023 13:54:05 -0400
Message-ID: <CAAA20URVxYOJ79UnmDT42Aga1xvoiRNkH+dy_+Pj0F7q5JUjPQ@mail.gmail.com>
Subject: Re: [PATCH v2] 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="00000000000096d2f505facdbef9"
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

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

On Tue, May 2, 2023 at 9:00=E2=80=AFAM Juraj Linke=C5=A1 <juraj.linkes@pant=
heon.tech>
wrote:

> On Fri, Apr 28, 2023 at 9:04=E2=80=AFPM Jeremy Spewock <jspewock@iol.unh.=
edu>
> wrote:
> >
> >
> >
> > On Mon, Apr 24, 2023 at 9:35=E2=80=AFAM Juraj Linke=C5=A1 <juraj.linkes=
@pantheon.tech>
> wrote:
> >>
> >> Pexpect is not a dedicated SSH connection library while Fabric is. Wit=
h
> >> 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>
> >> ---
> >>  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..d15826c098 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 environmen=
t
> and the other hosts.
> >> -   The pexpect implementation is a wrapper around the ssh command in
> the DTS environment.
> >> -   This means it'll use the SSH agent providing the ssh command and
> its keys.
> >> +   DTS uses the Fabric Python library for SSH connections between DTS
> environment
> >> +   and the other hosts.
> >> +   The authentication method used is pubkey authentication.
> >> +   Fabric tries to use a passed key/certificate,
> >> +   then any key it can with through an SSH agent,
> >> +   then any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in
> ``~/.ssh/``
> >> +   (with any matching OpenSSH-style certificates).
> >> +   DTS doesn't pass any keys, so Fabric tries to use the other two
> methods.
> >>
> >>
> >>  Setting up System Under Test
> >> @@ -132,6 +137,21 @@ There are two areas that need to be set up on a
> System Under Test:
> >>       It's possible to use the hugepage configuration already present
> on the SUT.
> >>       If you wish to do so, don't specify the hugepage configuration i=
n
> the DTS config file.
> >>
> >> +#. **User with administrator privileges**
> >> +
> >> +.. _sut_admin_user:
> >> +
> >> +   DTS needs administrator privileges to run DPDK applications (such
> as testpmd) on the SUT.
> >> +   The SUT user must be able run commands in privileged mode without
> asking for password.
> >> +   On most Linux distributions, it's a matter of setting up
> passwordless sudo:
> >> +
> >> +   #. Run ``sudo visudo`` and check that it contains ``%sudo
>  ALL=3D(ALL:ALL) 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/moun=
ts"
> >> -        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_sessi=
on
> >>
> >> @@ -53,17 +53,32 @@ def is_alive(self) -> bool:
> >>      def send_command(
> >>          self,
> >>          command: str,
> >> -        timeout: float,
> >> +        timeout: float =3D SETTINGS.timeout,
> >> +        privileged: bool =3D False,
> >>          verify: bool =3D False,
> >> -        env: EnvVarsDict | None =3D None,
> >> +        env: dict | None =3D None,
> >>      ) -> CommandResult:
> >>          """
> >>          An all-purpose API in case the command to be executed is
> already
> >>          OS-agnostic, such as when the path to the executed command ha=
s
> 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
> remote
> >> +        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_guess} | tail -1")
> >> +        result =3D self.send_command(f"ls -d {remote_guess} | tail -1=
")
> >>          return PurePosixPath(result.stdout)
> >>
> >>      def get_remote_tmp_dir(self) -> PurePosixPath:
> >> @@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architecture=
)
> -> dict:
> >>          env_vars =3D {}
> >>          if arch =3D=3D Architecture.i686:
> >>              # find the pkg-config path and store it in
> PKG_CONFIG_LIBDIR
> >> -            out =3D self.remote_session.send_command("find /usr -type=
 d
> -name pkgconfig")
> >> +            out =3D self.send_command("find /usr -type d -name
> pkgconfig")
> >>              pkg_path =3D ""
> >>              res_path =3D out.stdout.split("\r\n")
> >>              for cur_path in res_path:
> >> @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch:
> Architecture) -> dict:
> >>      def join_remote_path(self, *args: str | PurePath) -> PurePosixPat=
h:
> >>          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=
=3Dforce)
> >> -        self.remote_session.send_command(f"rm{opts} {remote_dir_path}=
")
> >> +        self.send_command(f"rm{opts} {remote_dir_path}")
> >>
> >>      def extract_remote_tarball(
> >>          self,
> >>          remote_tarball_path: str | PurePath,
> >>          expected_dir: str | PurePath | None =3D None,
> >>      ) -> None:
> >> -        self.remote_session.send_command(
> >> +        self.send_command(
> >>              f"tar xfm {remote_tarball_path} "
> >>              f"-C {PurePosixPath(remote_tarball_path).parent}",
> >>              60,
> >>          )
> >>          if expected_dir:
> >> -            self.remote_session.send_command(f"ls {expected_dir}",
> verify=3DTrue)
> >> +            self.send_command(f"ls {expected_dir}", verify=3DTrue)
> >>
> >>      def build_dpdk(
> >>          self,
> >> -        env_vars: EnvVarsDict,
> >> +        env_vars: dict,
> >>          meson_args: MesonArgs,
> >>          remote_dpdk_dir: str | PurePath,
> >>          remote_dpdk_build_dir: str | PurePath,
> >> @@ -108,7 +114,7 @@ def build_dpdk(
> >>              if rebuild:
> >>                  # reconfigure, then build
> >>                  self._logger.info("Reconfiguring DPDK build.")
> >> -                self.remote_session.send_command(
> >> +                self.send_command(
> >>                      f"meson configure {meson_args}
> {remote_dpdk_build_dir}",
> >>                      timeout,
> >>                      verify=3DTrue,
> >> @@ -118,7 +124,7 @@ def build_dpdk(
> >>                  # fresh build - remove target dir first, then build
> from scratch
> >>                  self._logger.info("Configuring DPDK build from
> scratch.")
> >>                  self.remove_remote_dir(remote_dpdk_build_dir)
> >> -                self.remote_session.send_command(
> >> +                self.send_command(
> >>                      f"meson setup "
> >>                      f"{meson_args} {remote_dpdk_dir}
> {remote_dpdk_build_dir}",
> >>                      timeout,
> >> @@ -127,14 +133,14 @@ def build_dpdk(
> >>                  )
> >>
> >>              self._logger.info("Building DPDK.")
> >> -            self.remote_session.send_command(
> >> +            self.send_command(
> >>                  f"ninja -C {remote_dpdk_build_dir}", timeout,
> verify=3DTrue, env=3Denv_vars
> >>              )
> >>          except RemoteCommandExecutionError as e:
> >>              raise DPDKBuildError(f"DPDK build failed when doing
> '{e.command}'.")
> >>
> >>      def get_dpdk_version(self, build_dir: str | PurePath) -> str:
> >> -        out =3D self.remote_session.send_command(
> >> +        out =3D self.send_command(
> >>              f"cat {self.join_remote_path(build_dir, 'VERSION')}",
> verify=3DTrue
> >>          )
> >>          return out.stdout
> >> @@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list:
> Iterable[str]) -> None:
> >>              # kill and cleanup only if DPDK is running
> >>              dpdk_pids =3D self._get_dpdk_pids(dpdk_runtime_dirs)
> >>              for dpdk_pid in dpdk_pids:
> >> -                self.remote_session.send_command(f"kill -9
> {dpdk_pid}", 20)
> >> +                self.send_command(f"kill -9 {dpdk_pid}", 20)
> >>              self._check_dpdk_hugepages(dpdk_runtime_dirs)
> >>              self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs)
> >>
> >> @@ -168,7 +174,7 @@ def _list_remote_dirs(self, remote_path: str |
> PurePath) -> list[str] | None:
> >>          Return a list of directories of the remote_dir.
> >>          If remote_path doesn't exist, return None.
> >>          """
> >> -        out =3D self.remote_session.send_command(
> >> +        out =3D self.send_command(
> >>              f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'"
> >>          ).stdout
> >>          if "No such file or directory" in out:
> >> @@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs:
> Iterable[str | PurePath]) -> list[in
> >>          for dpdk_runtime_dir in dpdk_runtime_dirs:
> >>              dpdk_config_file =3D PurePosixPath(dpdk_runtime_dir,
> "config")
> >>              if self._remote_files_exists(dpdk_config_file):
> >> -                out =3D self.remote_session.send_command(
> >> -                    f"lsof -Fp {dpdk_config_file}"
> >> -                ).stdout
> >> +                out =3D self.send_command(f"lsof -Fp
> {dpdk_config_file}").stdout
> >>                  if out and "No such file or directory" not in out:
> >>                      for out_line in out.splitlines():
> >>                          match =3D re.match(pid_regex, out_line)
> >> @@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs:
> Iterable[str | PurePath]) -> list[in
> >>          return pids
> >>
> >>      def _remote_files_exists(self, remote_path: PurePath) -> bool:
> >> -        result =3D self.remote_session.send_command(f"test -e
> {remote_path}")
> >> +        result =3D self.send_command(f"test -e {remote_path}")
> >>          return not result.return_code
> >>
> >>      def _check_dpdk_hugepages(
> >> @@ -202,9 +206,7 @@ def _check_dpdk_hugepages(
> >>          for dpdk_runtime_dir in dpdk_runtime_dirs:
> >>              hugepage_info =3D PurePosixPath(dpdk_runtime_dir,
> "hugepage_info")
> >>              if self._remote_files_exists(hugepage_info):
> >> -                out =3D self.remote_session.send_command(
> >> -                    f"lsof -Fp {hugepage_info}"
> >> -                ).stdout
> >> +                out =3D self.send_command(f"lsof -Fp
> {hugepage_info}").stdout
> >>                  if out and "No such file or directory" not in out:
> >>                      self._logger.warning("Some DPDK processes did not
> free hugepages.")
> >>
> self._logger.warning("*******************************************")
> >> diff --git a/dts/framework/remote_session/remote/remote_session.py
> b/dts/framework/remote_session/remote/remote_session.py
> >> index 91dee3cb4f..0647d93de4 100644
> >> --- a/dts/framework/remote_session/remote/remote_session.py
> >> +++ b/dts/framework/remote_session/remote/remote_session.py
> >> @@ -11,7 +11,6 @@
> >>  from framework.exception import RemoteCommandExecutionError
> >>  from framework.logger import DTSLOG
> >>  from framework.settings import SETTINGS
> >> -from framework.utils import EnvVarsDict
> >>
> >>
> >>  @dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)
> >> @@ -89,7 +88,7 @@ def send_command(
> >>          command: str,
> >>          timeout: float =3D SETTINGS.timeout,
> >>          verify: bool =3D False,
> >> -        env: EnvVarsDict | None =3D None,
> >> +        env: dict | None =3D None,
> >>      ) -> CommandResult:
> >>          """
> >>          Send a command to the connected node using optional env vars
> >> @@ -114,7 +113,7 @@ def send_command(
> >>
> >>      @abstractmethod
> >>      def _send_command(
> >> -        self, command: str, timeout: float, env: EnvVarsDict | None
> >> +        self, command: str, timeout: float, env: dict | None
> >>      ) -> CommandResult:
> >>          """
> >>          Use the underlying protocol to execute the command using
> 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
> remote
> >> +        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.
> >>
> >
> > I've noticed in other patches you've simply appended the copyright for
> PANTHEON.tech to the existing list. Is there a reason you remove the othe=
rs
> here as well?
> >
>
> It's a rewrite of the file. I'm the only author of the code (i.e.
> neither Intel nor UNH contributed to the Fabric code) so I left only
> us there. I'm not sure this is the right way to do this, but it made
> sense to me. I have no problem with leaving all parties in.
>
>
It also makes sense to me. I'm also not completely sure if it is the right
way to handle it, but the way I see it because the Copyrights exist in
every file it makes sense that they would be in the scope of that file.


> >>
> >> -import time
> >> +import socket
> >> +import traceback
> >>  from pathlib import PurePath
> >>
> >> -import pexpect  # type: ignore
> >> -from pexpect import pxssh  # type: ignore
> >> +from fabric import Connection  # type: ignore[import]
> >> +from invoke.exceptions import (  # type: ignore[import]
> >> +    CommandTimedOut,
> >> +    ThreadException,
> >> +    UnexpectedExit,
> >> +)
> >> +from paramiko.ssh_exception import (  # type: ignore[import]
> >> +    AuthenticationException,
> >> +    BadHostKeyException,
> >> +    NoValidConnectionsError,
> >> +    SSHException,
> >> +)
> >>
> >>  from framework.config import NodeConfiguration
> >>  from framework.exception import SSHConnectionError,
> SSHSessionDeadError, SSHTimeoutError
> >>  from framework.logger import DTSLOG
> >> -from framework.utils import GREEN, RED, EnvVarsDict
> >>
> >>  from .remote_session import CommandResult, RemoteSession
> >>
> >>
> >>  class SSHSession(RemoteSession):
> >> -    """
> >> -    Module for creating Pexpect SSH remote sessions.
> >> +    """A persistent SSH connection to a remote Node.
> >> +
> >> +    The connection is implemented with the Fabric Python library.
> >> +
> >> +    Args:
> >> +        node_config: The configuration of the Node to connect to.
> >> +        session_name: The name of the session.
> >> +        logger: The logger used for logging.
> >> +            This should be passed from the parent OSSession.
> >> +
> >> +    Attributes:
> >> +        session: The underlying Fabric SSH connection.
> >> +
> >> +    Raises:
> >> +        SSHConnectionError: The connection cannot be established.
> >>      """
> >>
> >> -    session: pxssh.pxssh
> >> -    magic_prompt: str
> >> +    session: Connection
> >>
> >>      def __init__(
> >>          self,
> >> @@ -31,218 +51,91 @@ def __init__(
> >>          session_name: str,
> >>          logger: DTSLOG,
> >>      ):
> >> -        self.magic_prompt =3D "MAGIC PROMPT"
> >>          super(SSHSession, self).__init__(node_config, session_name,
> logger)
> >>
> >>      def _connect(self) -> None:
> >> -        """
> >> -        Create connection to assigned node.
> >> -        """
> >> +        errors =3D []
> >>          retry_attempts =3D 10
> >>          login_timeout =3D 20 if self.port else 10
> >> -        password_regex =3D (
> >> -            r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password
> for .+:)"
> >> -        )
> >> -        try:
> >> -            for retry_attempt in range(retry_attempts):
> >> -                self.session =3D pxssh.pxssh(encoding=3D"utf-8")
> >> -                try:
> >> -                    self.session.login(
> >> -                        self.ip,
> >> -                        self.username,
> >> -                        self.password,
> >> -                        original_prompt=3D"[$#>]",
> >> -                        port=3Dself.port,
> >> -                        login_timeout=3Dlogin_timeout,
> >> -                        password_regex=3Dpassword_regex,
> >> -                    )
> >> -                    break
> >> -                except Exception as e:
> >> -                    self._logger.warning(e)
> >> -                    time.sleep(2)
> >> -                    self._logger.info(
> >> -                        f"Retrying connection: retry number
> {retry_attempt + 1}."
> >> -                    )
> >> -            else:
> >> -                raise Exception(f"Connection to {self.hostname}
> failed")
> >> -
> >> -            self.send_expect("stty -echo", "#")
> >> -            self.send_expect("stty columns 1000", "#")
> >> -            self.send_expect("bind 'set enable-bracketed-paste off'",
> "#")
> >> -        except Exception as e:
> >> -            self._logger.error(RED(str(e)))
> >> -            if getattr(self, "port", None):
> >> -                suggestion =3D (
> >> -                    f"\nSuggestion: Check if the firewall on
> {self.hostname} is "
> >> -                    f"stopped.\n"
> >> +        for retry_attempt in range(retry_attempts):
> >> +            try:
> >> +                self.session =3D Connection(
> >> +                    self.ip,
> >> +                    user=3Dself.username,
> >> +                    port=3Dself.port,
> >> +                    connect_kwargs=3D{"password": self.password},
> >> +                    connect_timeout=3Dlogin_timeout,
> >>                  )
> >> -                self._logger.info(GREEN(suggestion))
> >> -
> >> -            raise SSHConnectionError(self.hostname)
> >> +                self.session.open()
> >>
> >> -    def send_expect(
> >> -        self, command: str, prompt: str, timeout: float =3D 15, verif=
y:
> 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 $?", promp=
t,
> timeout)
> >> -                try:
> >> -                    retval =3D int(ret_status)
> >> -                    if retval:
> >> -                        self._logger.error(f"Command: {command}
> failure!")
> >> -                        self._logger.error(ret)
> >> -                        return retval
> >> -                    else:
> >> -                        return ret
> >> -                except ValueError:
> >> -                    return ret
> >> -            else:
> >> -                return ret
> >> -        except Exception as e:
> >> -            self._logger.error(
> >> -                f"Exception happened in [{command}] and output is "
> >> -                f"[{self._get_output()}]"
> >> -            )
> >> -            raise e
> >> -
> >> -    def send_expect_base(self, command: str, prompt: str, timeout:
> float) -> str:
> >> -        self._clean_session()
> >> -        original_prompt =3D self.session.PROMPT
> >> -        self.session.PROMPT =3D prompt
> >> -        self._send_line(command)
> >> -        self._prompt(command, timeout)
> >> -
> >> -        before =3D self._get_output()
> >> -        self.session.PROMPT =3D original_prompt
> >> -        return before
> >> -
> >> -    def _clean_session(self) -> None:
> >> -        self.session.PROMPT =3D self.magic_prompt
> >> -        self.get_output(timeout=3D0.01)
> >> -        self.session.PROMPT =3D self.session.UNIQUE_PROMPT
> >> -
> >> -    def _send_line(self, command: str) -> None:
> >> -        if not self.is_alive():
> >> -            raise SSHSessionDeadError(self.hostname)
> >> -        if len(command) =3D=3D 2 and command.startswith("^"):
> >> -            self.session.sendcontrol(command[1])
> >> -        else:
> >> -            self.session.sendline(command)
> >> +            except (ValueError, BadHostKeyException,
> AuthenticationException) as e:
> >> +                self._logger.exception(e)
> >> +                raise SSHConnectionError(self.hostname) from e
> >>
> >> -    def _prompt(self, command: str, timeout: float) -> None:
> >> -        if not self.session.prompt(timeout):
> >> -            raise SSHTimeoutError(command, self._get_output()) from
> None
> >> +            except (NoValidConnectionsError, socket.error,
> SSHException) as e:
> >> +                self._logger.debug(traceback.format_exc())
> >> +                self._logger.warning(e)
> >>
> >> -    def get_output(self, timeout: float =3D 15) -> str:
> >> -        """
> >> -        Get all output before timeout
> >> -        """
> >> -        try:
> >> -            self.session.prompt(timeout)
> >> -        except Exception:
> >> -            pass
> >> -
> >> -        before =3D self._get_output()
> >> -        self._flush()
> >> -
> >> -        return before
> >> +                error =3D repr(e)
> >> +                if error not in errors:
> >> +                    errors.append(error)
> >>
> >> -    def _get_output(self) -> str:
> >> -        if not self.is_alive():
> >> -            raise SSHSessionDeadError(self.hostname)
> >> -        before =3D self.session.before.rsplit("\r\n", 1)[0]
> >> -        if before =3D=3D "[PEXPECT]":
> >> -            return ""
> >> -        return before
> >> +                self._logger.info(
> >> +                    f"Retrying connection: retry number {retry_attemp=
t
> + 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, en=
v)
> >> -        return_code =3D int(self._send_command_get_output("echo $?",
> timeout, None))
> >> +        """Send a command and return the result of the execution.
> >>
> >> -        # we're capturing only stdout
> >> -        return CommandResult(self.name, command, output, "",
> return_code)
> >> +        Args:
> >> +            command: The command to execute.
> >> +            timeout: Wait at most this many seconds for the execution
> to complete.
> >> +            env: Extra environment variables that will be used in
> 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
> connecting"
> >> -        i: int =3D p.expect(
> >> -            [ssh_newkey, "[pP]assword", "# ", pexpect.EOF,
> pexpect.TIMEOUT], 120
> >> -        )
> >> -        if i =3D=3D 0:  # add once in trust list
> >> -            p.sendline("yes")
> >> -            i =3D p.expect([ssh_newkey, "[pP]assword", pexpect.EOF], =
2)
> >> -
> >> -        if i =3D=3D 1:
> >> -            time.sleep(0.5)
> >> -            p.sendline(self.password)
> >> -            p.expect("Exit status 0", 60)
> >> -        if i =3D=3D 4:
> >> -            self._logger.error("SCP TIMEOUT error %d" % i)
> >> -        p.close()
> >> +    def _close(self, force: bool =3D False) -> None:
> >> +        self.session.close()
> >> diff --git a/dts/framework/testbed_model/sut_node.py
> b/dts/framework/testbed_model/sut_node.py
> >> index 2b2b50d982..9dbc390848 100644
> >> --- a/dts/framework/testbed_model/sut_node.py
> >> +++ b/dts/framework/testbed_model/sut_node.py
> >> @@ -10,7 +10,7 @@
> >>  from framework.config import BuildTargetConfiguration,
> NodeConfiguration
> >>  from framework.remote_session import CommandResult, OSSession
> >>  from framework.settings import SETTINGS
> >> -from framework.utils import EnvVarsDict, MesonArgs
> >> +from framework.utils import MesonArgs
> >>
> >>  from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice
> >>  from .node import Node
> >> @@ -27,7 +27,7 @@ class SutNode(Node):
> >>      _dpdk_prefix_list: list[str]
> >>      _dpdk_timestamp: str
> >>      _build_target_config: BuildTargetConfiguration | None
> >> -    _env_vars: EnvVarsDict
> >> +    _env_vars: dict
> >>      _remote_tmp_dir: PurePath
> >>      __remote_dpdk_dir: PurePath | None
> >>      _dpdk_version: str | None
> >> @@ -38,7 +38,7 @@ def __init__(self, node_config: NodeConfiguration):
> >>          super(SutNode, self).__init__(node_config)
> >>          self._dpdk_prefix_list =3D []
> >>          self._build_target_config =3D None
> >> -        self._env_vars =3D EnvVarsDict()
> >> +        self._env_vars =3D {}
> >>          self._remote_tmp_dir =3D self.main_session.get_remote_tmp_dir=
()
> >>          self.__remote_dpdk_dir =3D None
> >>          self._dpdk_version =3D None
> >> @@ -94,7 +94,7 @@ def _configure_build_target(
> >>          """
> >>          Populate common environment variables and set build target
> 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", "py=
test
> (>=3D4.3.0)", "mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins",
> "zope.interface", "cloudpickle"]
> >>  tests_no_zope =3D ["coverage[toml] (>=3D5.0.2)", "hypothesis", "pympl=
er",
> "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 cryptograph=
ic
> recipes and primitives to Python developers."
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D ">=3D3.6"
> >> +
> >> +[package.dependencies]
> >> +cffi =3D ">=3D1.12"
> >> +
> >> +[package.extras]
> >> +docs =3D ["sphinx (>=3D5.3.0)", "sphinx-rtd-theme (>=3D1.1.1)"]
> >> +docstest =3D ["pyenchant (>=3D1.6.11)", "twine (>=3D1.12.0)",
> "sphinxcontrib-spelling (>=3D4.0.1)"]
> >> +pep8test =3D ["black", "ruff", "mypy", "check-manifest"]
> >> +sdist =3D ["setuptools-rust (>=3D0.11.4)"]
> >> +ssh =3D ["bcrypt (>=3D3.1.5)"]
> >> +test =3D ["pytest (>=3D6.2.0)", "pytest-shard (>=3D0.1.2)",
> "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist",
> "pretend", "iso8601"]
> >> +test-randomorder =3D ["pytest-randomly"]
> >> +tox =3D ["tox"]
> >> +
> >> +[[package]]
> >> +name =3D "fabric"
> >> +version =3D "2.7.1"
> >> +description =3D "High level SSH command execution"
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D "*"
> >> +
> >> +[package.dependencies]
> >> +invoke =3D ">=3D1.3,<2.0"
> >> +paramiko =3D ">=3D2.4"
> >> +pathlib2 =3D "*"
> >> +
> >> +[package.extras]
> >> +pytest =3D ["mock (>=3D2.0.0,<3.0)", "pytest (>=3D3.2.5,<4.0)"]
> >> +testing =3D ["mock (>=3D2.0.0,<3.0)"]
> >> +
> >> +[[package]]
> >> +name =3D "invoke"
> >> +version =3D "1.7.3"
> >> +description =3D "Pythonic task execution"
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D "*"
> >> +
> >>  [[package]]
> >>  name =3D "isort"
> >>  version =3D "5.10.1"
> >> @@ -136,23 +205,41 @@ optional =3D false
> >>  python-versions =3D "*"
> >>
> >>  [[package]]
> >> -name =3D "pathspec"
> >> -version =3D "0.10.1"
> >> -description =3D "Utility library for gitignore style pattern matching=
 of
> file paths."
> >> -category =3D "dev"
> >> +name =3D "paramiko"
> >> +version =3D "3.1.0"
> >> +description =3D "SSH2 protocol library"
> >> +category =3D "main"
> >>  optional =3D false
> >> -python-versions =3D ">=3D3.7"
> >> +python-versions =3D ">=3D3.6"
> >> +
> >> +[package.dependencies]
> >> +bcrypt =3D ">=3D3.2"
> >> +cryptography =3D ">=3D3.3"
> >> +pynacl =3D ">=3D1.5"
> >> +
> >> +[package.extras]
> >> +all =3D ["pyasn1 (>=3D0.1.7)", "invoke (>=3D2.0)", "gssapi (>=3D1.4.1=
)",
> "pywin32 (>=3D2.1.8)"]
> >> +gssapi =3D ["pyasn1 (>=3D0.1.7)", "gssapi (>=3D1.4.1)", "pywin32 (>=
=3D2.1.8)"]
> >> +invoke =3D ["invoke (>=3D2.0)"]
> >>
> >>  [[package]]
> >> -name =3D "pexpect"
> >> -version =3D "4.8.0"
> >> -description =3D "Pexpect allows easy control of interactive console
> applications."
> >> +name =3D "pathlib2"
> >> +version =3D "2.3.7.post1"
> >> +description =3D "Object-oriented filesystem paths"
> >>  category =3D "main"
> >>  optional =3D false
> >>  python-versions =3D "*"
> >>
> >>  [package.dependencies]
> >> -ptyprocess =3D ">=3D0.5"
> >> +six =3D "*"
> >> +
> >> +[[package]]
> >> +name =3D "pathspec"
> >> +version =3D "0.10.1"
> >> +description =3D "Utility library for gitignore style pattern matching=
 of
> file paths."
> >> +category =3D "dev"
> >> +optional =3D false
> >> +python-versions =3D ">=3D3.7"
> >>
> >>  [[package]]
> >>  name =3D "platformdirs"
> >> @@ -166,14 +253,6 @@ python-versions =3D ">=3D3.7"
> >>  docs =3D ["furo (>=3D2021.7.5b38)", "proselint (>=3D0.10.2)",
> "sphinx-autodoc-typehints (>=3D1.12)", "sphinx (>=3D4)"]
> >>  test =3D ["appdirs (=3D=3D1.4.4)", "pytest-cov (>=3D2.7)", "pytest-mo=
ck
> (>=3D3.6)", "pytest (>=3D6)"]
> >>
> >> -[[package]]
> >> -name =3D "ptyprocess"
> >> -version =3D "0.7.0"
> >> -description =3D "Run a subprocess in a pseudo terminal"
> >> -category =3D "main"
> >> -optional =3D false
> >> -python-versions =3D "*"
> >> -
> >>  [[package]]
> >>  name =3D "pycodestyle"
> >>  version =3D "2.9.1"
> >> @@ -182,6 +261,14 @@ category =3D "dev"
> >>  optional =3D false
> >>  python-versions =3D ">=3D3.6"
> >>
> >> +[[package]]
> >> +name =3D "pycparser"
> >> +version =3D "2.21"
> >> +description =3D "C parser in Python"
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*, !=3D3.=
3.*"
> >> +
> >>  [[package]]
> >>  name =3D "pydocstyle"
> >>  version =3D "6.1.1"
> >> @@ -228,6 +315,21 @@ tests =3D ["pytest (>=3D7.1.2)", "pytest-mypy",
> "eradicate (>=3D2.0.0)", "radon (>=3D5.1
> >>  toml =3D ["toml (>=3D0.10.2)"]
> >>  vulture =3D ["vulture"]
> >>
> >> +[[package]]
> >> +name =3D "pynacl"
> >> +version =3D "1.5.0"
> >> +description =3D "Python binding to the Networking and Cryptography
> (NaCl) library"
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D ">=3D3.6"
> >> +
> >> +[package.dependencies]
> >> +cffi =3D ">=3D1.4.1"
> >> +
> >> +[package.extras]
> >> +docs =3D ["sphinx (>=3D1.6.5)", "sphinx-rtd-theme"]
> >> +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)", "hypothesis (>=3D3.27.0)"]
> >> +
> >>  [[package]]
> >>  name =3D "pyrsistent"
> >>  version =3D "0.19.1"
> >> @@ -244,6 +346,14 @@ category =3D "main"
> >>  optional =3D false
> >>  python-versions =3D ">=3D3.6"
> >>
> >> +[[package]]
> >> +name =3D "six"
> >> +version =3D "1.16.0"
> >> +description =3D "Python 2 and 3 compatibility utilities"
> >> +category =3D "main"
> >> +optional =3D false
> >> +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*"
> >> +
> >>  [[package]]
> >>  name =3D "snowballstemmer"
> >>  version =3D "2.2.0"
> >> @@ -299,13 +409,18 @@ jsonschema =3D ">=3D4,<5"
> >>  [metadata]
> >>  lock-version =3D "1.1"
> >>  python-versions =3D "^3.10"
> >> -content-hash =3D
> "a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb9e260a67e92c2403e2319f"
> >> +content-hash =3D
> "719c43bcaa5d181921debda884f8f714063df0b2336d61e9f64ecab034e8b139"
> >>
> >>  [metadata.files]
> >>  attrs =3D []
> >> +bcrypt =3D []
> >>  black =3D []
> >> +cffi =3D []
> >>  click =3D []
> >>  colorama =3D []
> >> +cryptography =3D []
> >> +fabric =3D []
> >> +invoke =3D []
> >>  isort =3D []
> >>  jsonpatch =3D []
> >>  jsonpointer =3D []
> >> @@ -313,22 +428,22 @@ jsonschema =3D []
> >>  mccabe =3D []
> >>  mypy =3D []
> >>  mypy-extensions =3D []
> >> +paramiko =3D []
> >> +pathlib2 =3D []
> >>  pathspec =3D []
> >> -pexpect =3D [
> >> -    {file =3D "pexpect-4.8.0-py2.py3-none-any.whl", hash =3D
> "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"=
},
> >> -    {file =3D "pexpect-4.8.0.tar.gz", hash =3D
> "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"=
},
> >> -]
> >>  platformdirs =3D [
> >>      {file =3D "platformdirs-2.5.2-py3-none-any.whl", hash =3D
> "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"=
},
> >>      {file =3D "platformdirs-2.5.2.tar.gz", hash =3D
> "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"=
},
> >>  ]
> >> -ptyprocess =3D []
> >>  pycodestyle =3D []
> >> +pycparser =3D []
> >>  pydocstyle =3D []
> >>  pyflakes =3D []
> >>  pylama =3D []
> >> +pynacl =3D []
> >>  pyrsistent =3D []
> >>  pyyaml =3D []
> >> +six =3D []
> >>  snowballstemmer =3D []
> >>  toml =3D []
> >>  tomli =3D []
> >> diff --git a/dts/pyproject.toml b/dts/pyproject.toml
> >> index a136c91e5e..50bcdb327a 100644
> >> --- a/dts/pyproject.toml
> >> +++ b/dts/pyproject.toml
> >> @@ -9,10 +9,10 @@ authors =3D ["Owen Hilyard <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.30.2
> >>
>

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

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

<div dir=3D"ltr"><div dir=3D"ltr"></div><br><div class=3D"gmail_quote"><div=
 dir=3D"ltr" class=3D"gmail_attr">On Tue, May 2, 2023 at 9:00=E2=80=AFAM Ju=
raj Linke=C5=A1 &lt;juraj.linkes@pantheon.tech&gt; wrote:<br></div><blockqu=
ote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0.8ex;border-left:1px=
 solid rgb(204,204,204);padding-left:1ex">On Fri, Apr 28, 2023 at 9:04=E2=
=80=AFPM Jeremy Spewock &lt;<a href=3D"mailto:jspewock@iol.unh.edu" target=
=3D"_blank">jspewock@iol.unh.edu</a>&gt; wrote:<br>
&gt;<br>
&gt;<br>
&gt;<br>
&gt; On Mon, Apr 24, 2023 at 9:35=E2=80=AFAM Juraj Linke=C5=A1 &lt;juraj.li=
nkes@pantheon.tech&gt; wrote:<br>
&gt;&gt;<br>
&gt;&gt; Pexpect is not a dedicated SSH connection library while Fabric is.=
 With<br>
&gt;&gt; Fabric, all SSH-related logic is provided and we can just focus on=
<br>
&gt;&gt; what&#39;s DTS specific.<br>
&gt;&gt;<br>
&gt;&gt; Signed-off-by: Juraj Linke=C5=A1 &lt;juraj.linkes@pantheon.tech&gt=
;<br>
&gt;&gt; ---<br>
&gt;&gt;=C2=A0 doc/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>
&gt;&gt;=C2=A0 dts/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>
&gt;&gt;=C2=A0 dts/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>
&gt;&gt;=C2=A0 dts/framework/remote_session/linux_session.py |=C2=A0 31 +-<=
br>
&gt;&gt;=C2=A0 dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2=
=A0 51 +++-<br>
&gt;&gt;=C2=A0 dts/framework/remote_session/posix_session.py |=C2=A0 48 +--=
<br>
&gt;&gt;=C2=A0 .../remote_session/remote/remote_session.py=C2=A0 =C2=A0|=C2=
=A0 35 ++-<br>
&gt;&gt;=C2=A0 .../remote_session/remote/ssh_session.py=C2=A0 =C2=A0 =C2=A0=
 | 287 ++++++------------<br>
&gt;&gt;=C2=A0 dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 =
=C2=A0|=C2=A0 12 +-<br>
&gt;&gt;=C2=A0 dts/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>
&gt;&gt;=C2=A0 dts/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>
&gt;&gt;=C2=A0 dts/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>
&gt;&gt;=C2=A0 12 files changed, 376 insertions(+), 301 deletions(-)<br>
&gt;&gt;<br>
&gt;&gt; diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst<b=
r>
&gt;&gt; index ebd6dceb6a..d15826c098 100644<br>
&gt;&gt; --- a/doc/guides/tools/dts.rst<br>
&gt;&gt; +++ b/doc/guides/tools/dts.rst<br>
&gt;&gt; @@ -95,9 +95,14 @@ Setting up DTS environment<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 #. **SSH Connection**<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0DTS uses Python pexpect for SSH connections between =
DTS environment and the other hosts.<br>
&gt;&gt; -=C2=A0 =C2=A0The pexpect implementation is a wrapper around the s=
sh command in the DTS environment.<br>
&gt;&gt; -=C2=A0 =C2=A0This means it&#39;ll use the SSH agent providing the=
 ssh command and its keys.<br>
&gt;&gt; +=C2=A0 =C2=A0DTS uses the Fabric Python library for SSH connectio=
ns between DTS environment<br>
&gt;&gt; +=C2=A0 =C2=A0and the other hosts.<br>
&gt;&gt; +=C2=A0 =C2=A0The authentication method used is pubkey authenticat=
ion.<br>
&gt;&gt; +=C2=A0 =C2=A0Fabric tries to use a passed key/certificate,<br>
&gt;&gt; +=C2=A0 =C2=A0then any key it can with through an SSH agent,<br>
&gt;&gt; +=C2=A0 =C2=A0then any &quot;id_rsa&quot;, &quot;id_dsa&quot; or &=
quot;id_ecdsa&quot; key discoverable in ``~/.ssh/``<br>
&gt;&gt; +=C2=A0 =C2=A0(with any matching OpenSSH-style certificates).<br>
&gt;&gt; +=C2=A0 =C2=A0DTS doesn&#39;t pass any keys, so Fabric tries to us=
e the other two methods.<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 Setting up System Under Test<br>
&gt;&gt; @@ -132,6 +137,21 @@ There are two areas that need to be set up on=
 a System Under Test:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0It&#39;s possible to use the hugepage co=
nfiguration already present on the SUT.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0If you wish to do so, don&#39;t specify =
the hugepage configuration in the DTS config file.<br>
&gt;&gt;<br>
&gt;&gt; +#. **User with administrator privileges**<br>
&gt;&gt; +<br>
&gt;&gt; +.. _sut_admin_user:<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0DTS needs administrator privileges to run DPDK appli=
cations (such as testpmd) on the SUT.<br>
&gt;&gt; +=C2=A0 =C2=A0The SUT user must be able run commands in privileged=
 mode without asking for password.<br>
&gt;&gt; +=C2=A0 =C2=A0On most Linux distributions, it&#39;s a matter of se=
tting up passwordless sudo:<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0#. Run ``sudo visudo`` and check that it contains ``=
%sudo=C2=A0 =C2=A0ALL=3D(ALL:ALL) ALL``.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0#. Add the SUT user to the sudo group with:<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0.. code-block:: console<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 sudo usermod -aG sudo &lt;sut_user&gt;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 Running DTS<br>
&gt;&gt;=C2=A0 -----------<br>
&gt;&gt; @@ -151,7 +171,8 @@ which is a template that illustrates what can =
be configured in DTS:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0:start-at: executions:<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt; -The user must be root or any other user with prompt starting with=
 ``#``.<br>
&gt;&gt; +The user must have :ref:`administrator privileges &lt;sut_admin_u=
ser&gt;`<br>
&gt;&gt; +which don&#39;t require password authentication.<br>
&gt;&gt;=C2=A0 The other fields are mostly self-explanatory<br>
&gt;&gt;=C2=A0 and documented in more detail in ``dts/framework/config/conf=
_yaml_schema.json``.<br>
&gt;&gt;<br>
&gt;&gt; diff --git a/dts/conf.yaml b/dts/conf.yaml<br>
&gt;&gt; index a9bd8a3ecf..129801d87c 100644<br>
&gt;&gt; --- a/dts/conf.yaml<br>
&gt;&gt; +++ b/dts/conf.yaml<br>
&gt;&gt; @@ -16,7 +16,7 @@ executions:<br>
&gt;&gt;=C2=A0 nodes:<br>
&gt;&gt;=C2=A0 =C2=A0 - name: &quot;SUT 1&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 hostname: sut1.change.me.localhost<br>
&gt;&gt; -=C2=A0 =C2=A0 user: root<br>
&gt;&gt; +=C2=A0 =C2=A0 user: dtsuser<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 arch: x86_64<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 os: linux<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 lcores: &quot;&quot;<br>
&gt;&gt; diff --git a/dts/framework/exception.py b/dts/framework/exception.=
py<br>
&gt;&gt; index ca353d98fc..44ff4e979a 100644<br>
&gt;&gt; --- a/dts/framework/exception.py<br>
&gt;&gt; +++ b/dts/framework/exception.py<br>
&gt;&gt; @@ -62,13 +62,19 @@ class SSHConnectionError(DTSError):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 host: str<br>
&gt;&gt; +=C2=A0 =C2=A0 errors: list[str]<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 severity: ClassVar[ErrorSeverity] =3D ErrorSev=
erity.SSH_ERR<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def __init__(self, host: str):<br>
&gt;&gt; +=C2=A0 =C2=A0 def __init__(self, host: str, errors: list[str] | N=
one =3D None):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.host =3D host<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.errors =3D [] if errors is None =
else errors<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def __str__(self) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f&quot;Error trying to connect=
 with {self.host}&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 message =3D f&quot;Error trying to co=
nnect with {self.host}.&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.errors:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message +=3D f&quot; Er=
rors encountered while retrying: {&#39;, &#39;.join(self.errors)}&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return message<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 class SSHSessionDeadError(DTSError):<br>
&gt;&gt; diff --git a/dts/framework/remote_session/linux_session.py b/dts/f=
ramework/remote_session/linux_session.py<br>
&gt;&gt; index a1e3bc3a92..f13f399121 100644<br>
&gt;&gt; --- a/dts/framework/remote_session/linux_session.py<br>
&gt;&gt; +++ b/dts/framework/remote_session/linux_session.py<br>
&gt;&gt; @@ -14,10 +14,11 @@ class LinuxSession(PosixSession):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 The implementation of non-Posix compliant part=
s of Linux remote sessions.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt; +=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&g=
t; str:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f&quot;sudo -- sh -c &#39;{com=
mand}&#39;&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def get_remote_cpus(self, use_first_core: bool=
) -&gt; list[LogicalCore]:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.remote_session.send=
_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;lscpu -p=3DCPU,CO=
RE,SOCKET,NODE|grep -v \\#&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
&gt;&gt; +=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>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcores =3D []<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu_line in cpu_info.splitli=
nes():<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcore, core, socke=
t, node =3D map(int, cpu_line.split(&quot;,&quot;))<br>
&gt;&gt; @@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int=
, force_first_numa: bool) -&gt; None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._mount_huge_pages()<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _get_hugepage_size(self) -&gt; int:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.remote_session=
.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.send_command(<=
br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;awk &#39;/Hu=
gepagesize/ {print $2}&#39; /proc/meminfo&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return int(hugepage_size)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _get_hugepages_total(self) -&gt; int:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.remote_sessi=
on.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.send_command=
(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;awk &#39;/Hu=
gePages_Total/ { print $2 }&#39; /proc/meminfo&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return int(hugepages_total)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _get_numa_nodes(self) -&gt; list[int]:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.rem=
ote_session.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.sen=
d_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quo=
t;cat /sys/devices/system/node/online&quot;, verify=3DTrue<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_range =3D exp=
and_range(numa_count)<br>
&gt;&gt; @@ -70,14 +71,12 @@ def _get_numa_nodes(self) -&gt; list[int]:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _mount_huge_pages(self) -&gt; None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"http://logger.i=
nfo" rel=3D"noreferrer" target=3D"_blank">logger.info</a>(&quot;Re-mounting=
 Hugepages.&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugapge_fs_cmd =3D &quot;awk &#3=
9;/hugetlbfs/ { print $2 }&#39; /proc/mounts&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&qu=
ot;umount $({hugapge_fs_cmd})&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c=
ommand(hugapge_fs_cmd)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;umount $({hu=
gapge_fs_cmd})&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(hugapge_=
fs_cmd)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if result.stdout =3D=3D &quot;&q=
uot;:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_mount_path =
=3D &quot;/mnt/huge&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen=
d_command(f&quot;mkdir -p {remote_mount_path}&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen=
d_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;mo=
unt -t hugetlbfs nodev {remote_mount_path}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo=
t;mkdir -p {remote_mount_path}&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo=
t;mount -t hugetlbfs nodev {remote_mount_path}&quot;)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _supports_numa(self) -&gt; bool:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # the system supports numa if se=
lf._numa_nodes is non-empty and there are more<br>
&gt;&gt; @@ -94,14 +93,12 @@ def _configure_huge_pages(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if force_first_numa and self._su=
pports_numa():<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # clear non-numa h=
ugepages<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen=
d_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;ec=
ho 0 | sudo tee {hugepage_config_path}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo=
t;echo 0 | tee {hugepage_config_path}&quot;, privileged=3DTrue)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_config_pa=
th =3D (<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu=
ot;/sys/devices/system/node/node{self._numa_nodes[0]}/hugepages&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu=
ot;/hugepages-{size}kB/nr_hugepages&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;echo {amount} | =
sudo tee {hugepage_config_path}&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;echo {amount} | =
tee {hugepage_config_path}&quot;, privileged=3DTrue<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; diff --git a/dts/framework/remote_session/os_session.py b/dts/fram=
ework/remote_session/os_session.py<br>
&gt;&gt; index 4c48ae2567..bfd70bd480 100644<br>
&gt;&gt; --- a/dts/framework/remote_session/os_session.py<br>
&gt;&gt; +++ b/dts/framework/remote_session/os_session.py<br>
&gt;&gt; @@ -10,7 +10,7 @@<br>
&gt;&gt;=C2=A0 from framework.logger import DTSLOG<br>
&gt;&gt;=C2=A0 from framework.settings import SETTINGS<br>
&gt;&gt;=C2=A0 from framework.testbed_model import LogicalCore<br>
&gt;&gt; -from framework.utils import EnvVarsDict, MesonArgs<br>
&gt;&gt; +from framework.utils import MesonArgs<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 from .remote import CommandResult, RemoteSession, create_rem=
ote_session<br>
&gt;&gt;<br>
&gt;&gt; @@ -53,17 +53,32 @@ def is_alive(self) -&gt; bool:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: str,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged: bool =3D False,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify: bool =3D False,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; CommandResult:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 An all-purpose API in case the c=
ommand to be executed is already<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 OS-agnostic, such as when the pa=
th to the executed command has been<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 constructed beforehand.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if privileged:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D self._get_p=
rivileged_command(command)<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.remote_session.send_=
command(command, timeout, verify, env)<br>
&gt;&gt;<br>
&gt;&gt; +=C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; +=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&g=
t; str:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Modify the command =
so that it executes with administrative privileges.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to=
 modify.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The modified command th=
at executes with administrative privileges.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def guess_dpdk_remote_dir(self, remote_dir) -&=
gt; PurePath:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; @@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PureP=
ath) -&gt; PurePath:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; -=C2=A0 =C2=A0 def copy_file(<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_from(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath=
,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from th=
e remote Node to the local filesystem.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node=
 associated with this remote<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the lo=
cal filesystem.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o=
n the remote Node.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil=
e or directory path on the local filesystem.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_to(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from lo=
cal filesystem to the remote Node.<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local file=
system to destination_file<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th=
e remote session.<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the=
 direction - copy source_file from the<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated remote Node to destination=
_file on local storage.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th=
is remote session.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o=
n the local filesystem.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil=
e or directory path on the remote Node.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; @@ -128,7 +161,7 @@ def extract_remote_tarball(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def build_dpdk(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 meson_args: MesonArgs,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_dir: str | PurePath,=
<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_build_dir: str | Pur=
ePath,<br>
&gt;&gt; diff --git a/dts/framework/remote_session/posix_session.py b/dts/f=
ramework/remote_session/posix_session.py<br>
&gt;&gt; index d38062e8d6..8ca0acb429 100644<br>
&gt;&gt; --- a/dts/framework/remote_session/posix_session.py<br>
&gt;&gt; +++ b/dts/framework/remote_session/posix_session.py<br>
&gt;&gt; @@ -9,7 +9,7 @@<br>
&gt;&gt;=C2=A0 from framework.config import Architecture<br>
&gt;&gt;=C2=A0 from framework.exception import DPDKBuildError, RemoteComman=
dExecutionError<br>
&gt;&gt;=C2=A0 from framework.settings import SETTINGS<br>
&gt;&gt; -from framework.utils import EnvVarsDict, MesonArgs<br>
&gt;&gt; +from framework.utils import MesonArgs<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 from .os_session import OSSession<br>
&gt;&gt;<br>
&gt;&gt; @@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -&gt; st=
r:<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def guess_dpdk_remote_dir(self, remote_dir) -&=
gt; PurePosixPath:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_guess =3D self.join_remot=
e_path(remote_dir, &quot;dpdk-*&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c=
ommand(f&quot;ls -d {remote_guess} | tail -1&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f&quot;l=
s -d {remote_guess} | tail -1&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return PurePosixPath(result.stdo=
ut)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def get_remote_tmp_dir(self) -&gt; PurePosixPa=
th:<br>
&gt;&gt; @@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architec=
ture) -&gt; dict:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars =3D {}<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if arch =3D=3D Architecture.i686=
:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # find the pkg-con=
fig path and store it in PKG_CONFIG_LIBDIR<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_ses=
sion.send_command(&quot;find /usr -type d -name pkgconfig&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_comma=
nd(&quot;find /usr -type d -name pkgconfig&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pkg_path =3D &quot=
;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 res_path =3D out.s=
tdout.split(&quot;\r\n&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for cur_path in re=
s_path:<br>
&gt;&gt; @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Archit=
ecture) -&gt; dict:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def join_remote_path(self, *args: str | PurePa=
th) -&gt; PurePosixPath:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return PurePosixPath(*args)<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def copy_file(<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_from(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath=
,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_file(source_=
file, destination_file, source_remote)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_from(source_=
file, destination_file)<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_to(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_to(source_fi=
le, destination_file)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def remove_remote_dir(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; @@ -80,24 +86,24 @@ def remove_remote_dir(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 force: bool =3D True,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 opts =3D PosixSession.combine_sh=
ort_options(r=3Drecursive, f=3Dforce)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&qu=
ot;rm{opts} {remote_dir_path}&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quot;rm{opts} {re=
mote_dir_path}&quot;)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def extract_remote_tarball(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_tarball_path: str | PureP=
ath,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 expected_dir: str | PurePath | N=
one =3D None,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;tar xfm {re=
mote_tarball_path} &quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;-C {PurePos=
ixPath(remote_tarball_path).parent}&quot;,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 60,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if expected_dir:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen=
d_command(f&quot;ls {expected_dir}&quot;, verify=3DTrue)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo=
t;ls {expected_dir}&quot;, verify=3DTrue)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def build_dpdk(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 meson_args: MesonArgs,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_dir: str | PurePath,=
<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_build_dir: str | Pur=
ePath,<br>
&gt;&gt; @@ -108,7 +114,7 @@ def build_dpdk(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if rebuild:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # re=
configure, then build<br>
&gt;&gt;=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>(&quot;Reconfiguring DPDK build.&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo=
te_session.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send=
_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 f&quot;meson configure {meson_args} {remote_dpdk_build_dir}&quot=
;,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 timeout,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 verify=3DTrue,<br>
&gt;&gt; @@ -118,7 +124,7 @@ def build_dpdk(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # fr=
esh build - remove target dir first, then build from scratch<br>
&gt;&gt;=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>(&quot;Configuring DPDK build from scratch.&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self=
.remove_remote_dir(remote_dpdk_build_dir)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo=
te_session.send_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send=
_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 f&quot;meson setup &quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 f&quot;{meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}&qu=
ot;,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 timeout,<br>
&gt;&gt; @@ -127,14 +133,14 @@ def build_dpdk(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br=
>
&gt;&gt;<br>
&gt;&gt;=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>(&qu=
ot;Building DPDK.&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen=
d_command(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu=
ot;ninja -C {remote_dpdk_build_dir}&quot;, timeout, verify=3DTrue, env=3Den=
v_vars<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except RemoteCommandExecutionErr=
or as e:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise DPDKBuildErr=
or(f&quot;DPDK build failed when doing &#39;{e.command}&#39;.&quot;)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def get_dpdk_version(self, build_dir: str | Pu=
rePath) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_comm=
and(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;cat {self.j=
oin_remote_path(build_dir, &#39;VERSION&#39;)}&quot;, verify=3DTrue<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return out.stdout<br>
&gt;&gt; @@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_l=
ist: Iterable[str]) -&gt; None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # kill and cleanup=
 only if DPDK is running<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_pids =3D self=
._get_dpdk_pids(dpdk_runtime_dirs)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_pid in dp=
dk_pids:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo=
te_session.send_command(f&quot;kill -9 {dpdk_pid}&quot;, 20)<br>
&gt;&gt; +=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>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._check_dpdk_h=
ugepages(dpdk_runtime_dirs)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remove_dpdk_=
runtime_dirs(dpdk_runtime_dirs)<br>
&gt;&gt;<br>
&gt;&gt; @@ -168,7 +174,7 @@ def _list_remote_dirs(self, remote_path: str |=
 PurePath) -&gt; list[str] | None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Return a list of directories of =
the remote_dir.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If remote_path doesn&#39;t exist=
, return None.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_comm=
and(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;ls -l {remo=
te_path} | awk &#39;/^d/ {{print $NF}}&#39;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if &quot;No such file or directo=
ry&quot; in out:<br>
&gt;&gt; @@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: It=
erable[str | PurePath]) -&gt; list[in<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_run=
time_dirs:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_config_file =
=3D PurePosixPath(dpdk_runtime_dir, &quot;config&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_fi=
les_exists(dpdk_config_file):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s=
elf.remote_session.send_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 f&quot;lsof -Fp {dpdk_config_file}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s=
elf.send_command(f&quot;lsof -Fp {dpdk_config_file}&quot;).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if o=
ut and &quot;No such file or directory&quot; not in out:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 for out_line in out.splitlines():<br>
&gt;&gt;=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 match =3D re.match(pid_regex, out_line)<br>
&gt;&gt; @@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: It=
erable[str | PurePath]) -&gt; list[in<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return pids<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _remote_files_exists(self, remote_path: Pu=
rePath) -&gt; bool:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c=
ommand(f&quot;test -e {remote_path}&quot;)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f&quot;t=
est -e {remote_path}&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return not result.return_code<br=
>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _check_dpdk_hugepages(<br>
&gt;&gt; @@ -202,9 +206,7 @@ def _check_dpdk_hugepages(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_run=
time_dirs:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_info =3D =
PurePosixPath(dpdk_runtime_dir, &quot;hugepage_info&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_fi=
les_exists(hugepage_info):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s=
elf.remote_session.send_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 f&quot;lsof -Fp {hugepage_info}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s=
elf.send_command(f&quot;lsof -Fp {hugepage_info}&quot;).stdout<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if o=
ut and &quot;No such file or directory&quot; not in out:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 self._logger.warning(&quot;Some DPDK processes did not free huge=
pages.&quot;)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 self._logger.warning(&quot;*************************************=
******&quot;)<br>
&gt;&gt; diff --git a/dts/framework/remote_session/remote/remote_session.py=
 b/dts/framework/remote_session/remote/remote_session.py<br>
&gt;&gt; index 91dee3cb4f..0647d93de4 100644<br>
&gt;&gt; --- a/dts/framework/remote_session/remote/remote_session.py<br>
&gt;&gt; +++ b/dts/framework/remote_session/remote/remote_session.py<br>
&gt;&gt; @@ -11,7 +11,6 @@<br>
&gt;&gt;=C2=A0 from framework.exception import RemoteCommandExecutionError<=
br>
&gt;&gt;=C2=A0 from framework.logger import DTSLOG<br>
&gt;&gt;=C2=A0 from framework.settings import SETTINGS<br>
&gt;&gt; -from framework.utils import EnvVarsDict<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 @dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)<br>
&gt;&gt; @@ -89,7 +88,7 @@ def send_command(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: str,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.time=
out,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify: bool =3D False,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; CommandResult:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a command to the connected =
node using optional env vars<br>
&gt;&gt; @@ -114,7 +113,7 @@ def send_command(<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _send_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e=
nv: EnvVarsDict | None<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e=
nv: dict | None<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; CommandResult:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Use the underlying protocol to e=
xecute the command using optional env vars<br>
&gt;&gt; @@ -141,15 +140,33 @@ def is_alive(self) -&gt; bool:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; -=C2=A0 =C2=A0 def copy_file(<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_from(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath=
,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from th=
e remote Node to the local filesystem.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node=
 associated with this remote<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the lo=
cal filesystem.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o=
n the remote Node.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil=
e or directory path on the local filesystem.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesyste=
m to destination_file on the remote Node<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated with the remote session.<b=
r>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the=
 direction - copy source_file from the<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated Node to destination_file o=
n local filesystem.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 @abstractmethod<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_to(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Copy a file from lo=
cal filesystem to the remote Node.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesyste=
m to destination_file<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th=
is remote session.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o=
n the local filesystem.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil=
e or directory path on the remote Node.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; diff --git a/dts/framework/remote_session/remote/ssh_session.py b/=
dts/framework/remote_session/remote/ssh_session.py<br>
&gt;&gt; index 42ff9498a2..8d127f1601 100644<br>
&gt;&gt; --- a/dts/framework/remote_session/remote/ssh_session.py<br>
&gt;&gt; +++ b/dts/framework/remote_session/remote/ssh_session.py<br>
&gt;&gt; @@ -1,29 +1,49 @@<br>
&gt;&gt;=C2=A0 # SPDX-License-Identifier: BSD-3-Clause<br>
&gt;&gt; -# Copyright(c) 2010-2014 Intel Corporation<br>
&gt;&gt; -# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.<br>
&gt;&gt; -# Copyright(c) 2022-2023 University of New Hampshire<br>
&gt;&gt; +# Copyright(c) 2023 PANTHEON.tech s.r.o.<br>
&gt;&gt;<br>
&gt;<br>
&gt; I&#39;ve noticed in other patches you&#39;ve simply appended the copyr=
ight for PANTHEON.tech to the existing list. Is there a reason you remove t=
he others here as well?<br>
&gt;<br>
<br>
It&#39;s a rewrite of the file. I&#39;m the only author of the code (i.e.<b=
r>
neither Intel nor UNH contributed to the Fabric code) so I left only<br>
us there. I&#39;m not sure this is the right way to do this, but it made<br=
>
sense to me. I have no problem with leaving all parties in.<br>
<br></blockquote><div><br></div><div>It also makes sense to me. I&#39;m als=
o not completely sure if it is the right way to handle it, but the way I se=
e it because the Copyrights exist in every file it makes sense that they wo=
uld be in the scope of that file.</div><div>=C2=A0</div><blockquote class=
=3D"gmail_quote" style=3D"margin:0px 0px 0px 0.8ex;border-left:1px solid rg=
b(204,204,204);padding-left:1ex">
&gt;&gt;<br>
&gt;&gt; -import time<br>
&gt;&gt; +import socket<br>
&gt;&gt; +import traceback<br>
&gt;&gt;=C2=A0 from pathlib import PurePath<br>
&gt;&gt;<br>
&gt;&gt; -import pexpect=C2=A0 # type: ignore<br>
&gt;&gt; -from pexpect import pxssh=C2=A0 # type: ignore<br>
&gt;&gt; +from fabric import Connection=C2=A0 # type: ignore[import]<br>
&gt;&gt; +from invoke.exceptions import (=C2=A0 # type: ignore[import]<br>
&gt;&gt; +=C2=A0 =C2=A0 CommandTimedOut,<br>
&gt;&gt; +=C2=A0 =C2=A0 ThreadException,<br>
&gt;&gt; +=C2=A0 =C2=A0 UnexpectedExit,<br>
&gt;&gt; +)<br>
&gt;&gt; +from paramiko.ssh_exception import (=C2=A0 # type: ignore[import]=
<br>
&gt;&gt; +=C2=A0 =C2=A0 AuthenticationException,<br>
&gt;&gt; +=C2=A0 =C2=A0 BadHostKeyException,<br>
&gt;&gt; +=C2=A0 =C2=A0 NoValidConnectionsError,<br>
&gt;&gt; +=C2=A0 =C2=A0 SSHException,<br>
&gt;&gt; +)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 from framework.config import NodeConfiguration<br>
&gt;&gt;=C2=A0 from framework.exception import SSHConnectionError, SSHSessi=
onDeadError, SSHTimeoutError<br>
&gt;&gt;=C2=A0 from framework.logger import DTSLOG<br>
&gt;&gt; -from framework.utils import GREEN, RED, EnvVarsDict<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 from .remote_session import CommandResult, RemoteSession<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 class SSHSession(RemoteSession):<br>
&gt;&gt; -=C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 Module for creating Pexpect SSH remote sessions.<br=
>
&gt;&gt; +=C2=A0 =C2=A0 &quot;&quot;&quot;A persistent SSH connection to a =
remote Node.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 The connection is implemented with the Fabric Pytho=
n library.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The configuration of the=
 Node to connect to.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: The name of the session=
.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: The logger used for logging.<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 This should be passed f=
rom the parent OSSession.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 Attributes:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session: The underlying Fabric SSH co=
nnection.<br>
&gt;&gt; +<br>
&gt;&gt; +=C2=A0 =C2=A0 Raises:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHConnectionError: The connection ca=
nnot be established.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 session: pxssh.pxssh<br>
&gt;&gt; -=C2=A0 =C2=A0 magic_prompt: str<br>
&gt;&gt; +=C2=A0 =C2=A0 session: Connection<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def __init__(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; @@ -31,218 +51,91 @@ def __init__(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: str,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: DTSLOG,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.magic_prompt =3D &quot;MAGIC PRO=
MPT&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(SSHSession, self).__init__=
(node_config, session_name, logger)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _connect(self) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Create connection to assigned node.<b=
r>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 errors =3D []<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 retry_attempts =3D 10<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 login_timeout =3D 20 if self.por=
t else 10<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 password_regex =3D (<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 r&quot;(?i)(?:password:=
)|(?:passphrase for key)|(?i)(password for .+:)&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in ra=
nge(retry_attempts):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess=
ion =3D pxssh.pxssh(encoding=3D&quot;utf-8&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 self.session.login(<br>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 )<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 break<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Ex=
ception as e:<br>
&gt;&gt; -=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>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 time.sleep(2)<br>
&gt;&gt; -=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"_b=
lank">logger.info</a>(<br>
&gt;&gt; -=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_attemp=
t + 1}.&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 )<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise Exc=
eption(f&quot;Connection to {self.hostname} failed&quot;)<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect(&quot;=
stty -echo&quot;, &quot;#&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect(&quot;=
stty columns 1000&quot;, &quot;#&quot;)<br>
&gt;&gt; -=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>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(RED(=
str(e)))<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if getattr(self, &quot;=
port&quot;, None):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 suggestio=
n =3D (<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 f&quot;\nSuggestion: Check if the firewall on {self.hostname} is &qu=
ot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 f&quot;stopped.\n&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry_atte=
mpts):<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess=
ion =3D Connection(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 self.ip,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 user=3Dself.username,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 port=3Dself.port,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 connect_kwargs=3D{&quot;password&quot;: self.password},<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 connect_timeout=3Dlogin_timeout,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br=
>
&gt;&gt; -=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">logger.inf=
o</a>(GREEN(suggestion))<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionErro=
r(self.hostname)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess=
ion.open()<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def send_expect(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, prompt: str, time=
out: float =3D 15, verify: bool =3D False<br>
&gt;&gt; -=C2=A0 =C2=A0 ) -&gt; str | int:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret =3D self.send_expec=
t_base(command, prompt, timeout)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if verify:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret_statu=
s =3D self.send_expect_base(&quot;echo $?&quot;, prompt, timeout)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 retval =3D int(ret_status)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 if retval:<br>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=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>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 else:<br>
&gt;&gt; -=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>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Va=
lueError:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 return ret<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return re=
t<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;Ex=
ception happened in [{command}] and output is &quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;[{=
self._get_output()}]&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 def send_expect_base(self, command: str, prompt: st=
r, timeout: float) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 original_prompt =3D self.session.PROM=
PT<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D prompt<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prompt(command, timeout)<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D original_prom=
pt<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 def _clean_session(self) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.magic_pr=
ompt<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.get_output(timeout=3D0.01)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.=
UNIQUE_PROMPT<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 def _send_line(self, command: str) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr=
or(self.hostname)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(command) =3D=3D 2 and command.=
startswith(&quot;^&quot;):<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendcontro=
l(command[1])<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendline(c=
ommand)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (ValueError, Bad=
HostKeyException, AuthenticationException) as e:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log=
ger.exception(e)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSH=
ConnectionError(self.hostname) from e<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _prompt(self, command: str, timeout: float) -&g=
t; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.session.prompt(timeout):<=
br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(c=
ommand, self._get_output()) from None<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (NoValidConnecti=
onsError, socket.error, SSHException) as e:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log=
ger.debug(traceback.format_exc())<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log=
ger.warning(e)<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def get_output(self, timeout: float =3D 15) -&gt; s=
tr:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get all output before timeout<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(tim=
eout)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._flush()<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 error =3D=
 repr(e)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if error =
not in errors:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 errors.append(error)<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _get_output(self) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr=
or(self.hostname)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self.session.before.rsplit=
(&quot;\r\n&quot;, 1)[0]<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if before =3D=3D &quot;[PEXPECT]&quot=
;:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return &quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before<br>
&gt;&gt; +=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">logger.inf=
o</a>(<br>
&gt;&gt; +=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}.&quot;<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _flush(self) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Clear all session buffer<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.buffer =3D &quot;&quot;<=
br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.before =3D &quot;&quot;<=
br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionErro=
r(self.hostname, errors)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def is_alive(self) -&gt; bool:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.isalive()<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.is_connected<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 def _send_command(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e=
nv: EnvVarsDict | None<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e=
nv: dict | None<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; CommandResult:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self._send_command_get_out=
put(command, timeout, env)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return_code =3D int(self._send_comman=
d_get_output(&quot;echo $?&quot;, timeout, None))<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;Send a command and =
return the result of the execution.<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 # we&#39;re capturing only stdout<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(<a href=3D"http:=
//self.name" rel=3D"noreferrer" target=3D"_blank">self.name</a>, command, o=
utput, &quot;&quot;, return_code)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to=
 execute.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Wait at most t=
his many seconds for the execution to complete.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env: Extra environment =
variables that will be used in command execution.<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _send_command_get_output(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e=
nv: EnvVarsDict | None<br>
&gt;&gt; -=C2=A0 =C2=A0 ) -&gt; str:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHSessionDeadError: Th=
e session died while executing the command.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHTimeoutError: The co=
mmand execution timed out.<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()<b=
r>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if env:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =
=3D f&quot;{env} {command}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command=
)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.session=
.run(<br>
&gt;&gt; +=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>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.get_output(timeout=3D=
timeout)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.=
UNIQUE_PROMPT<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(0.1)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (UnexpectedExit, ThreadExcepti=
on) as e:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(=
e)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr=
or(self.hostname) from e<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return output<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except CommandTimedOut as e:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(=
e)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(c=
ommand, e.result.stderr) from e<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -&gt; None:=
<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if force is True:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()<br=
>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.is_alive():<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess=
ion.logout()<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(<br>
&gt;&gt; +=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.s=
tdout, output.stderr, output.return_code<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def copy_file(<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_from(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath=
,<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a local file to a remote host.<b=
r>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if source_remote:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file =3D f&quot;=
{self.username}@{self.ip}:{source_file}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file =3D f&=
quot;{self.username}@{self.ip}:{destination_file}&quot;<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.get(str(destination_file=
), str(source_file))<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D &quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.port:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D f&quot; -P {se=
lf.port}&quot;<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D (<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;scp -v{port} -o =
NoHostAuthenticationForLocalhost=3Dyes&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot; {source_file} {=
destination_file}&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._spawn_scp(command)<br>
&gt;&gt; +=C2=A0 =C2=A0 def copy_to(<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,<br>
&gt;&gt; +=C2=A0 =C2=A0 ) -&gt; None:<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.put(str(source_file), st=
r(destination_file))<br>
&gt;&gt;<br>
&gt;&gt; -=C2=A0 =C2=A0 def _spawn_scp(self, scp_cmd: str) -&gt; None:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Transfer a file with SCP<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"http://logger.info" =
rel=3D"noreferrer" target=3D"_blank">logger.info</a>(scp_cmd)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 p: pexpect.spawn =3D pexpect.spawn(sc=
p_cmd)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 ssh_newkey: str =3D &quot;Are you sur=
e you want to continue connecting&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 i: int =3D p.expect(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [ssh_newkey, &quot;[pP]=
assword&quot;, &quot;# &quot;, pexpect.EOF, pexpect.TIMEOUT], 120<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 0:=C2=A0 # add once in tr=
ust list<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline(&quot;yes&qu=
ot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 i =3D p.expect([ssh_new=
key, &quot;[pP]assword&quot;, pexpect.EOF], 2)<br>
&gt;&gt; -<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 1:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline(self.passwor=
d)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.expect(&quot;Exit sta=
tus 0&quot;, 60)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 4:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(&quo=
t;SCP TIMEOUT error %d&quot; % i)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 p.close()<br>
&gt;&gt; +=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -&gt; None:=
<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()<br>
&gt;&gt; diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framewo=
rk/testbed_model/sut_node.py<br>
&gt;&gt; index 2b2b50d982..9dbc390848 100644<br>
&gt;&gt; --- a/dts/framework/testbed_model/sut_node.py<br>
&gt;&gt; +++ b/dts/framework/testbed_model/sut_node.py<br>
&gt;&gt; @@ -10,7 +10,7 @@<br>
&gt;&gt;=C2=A0 from framework.config import BuildTargetConfiguration, NodeC=
onfiguration<br>
&gt;&gt;=C2=A0 from framework.remote_session import CommandResult, OSSessio=
n<br>
&gt;&gt;=C2=A0 from framework.settings import SETTINGS<br>
&gt;&gt; -from framework.utils import EnvVarsDict, MesonArgs<br>
&gt;&gt; +from framework.utils import MesonArgs<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 from .hw import LogicalCoreCount, LogicalCoreList, VirtualDe=
vice<br>
&gt;&gt;=C2=A0 from .node import Node<br>
&gt;&gt; @@ -27,7 +27,7 @@ class SutNode(Node):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 _dpdk_prefix_list: list[str]<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 _dpdk_timestamp: str<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 _build_target_config: BuildTargetConfiguration=
 | None<br>
&gt;&gt; -=C2=A0 =C2=A0 _env_vars: EnvVarsDict<br>
&gt;&gt; +=C2=A0 =C2=A0 _env_vars: dict<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 _remote_tmp_dir: PurePath<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 __remote_dpdk_dir: PurePath | None<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 _dpdk_version: str | None<br>
&gt;&gt; @@ -38,7 +38,7 @@ def __init__(self, node_config: NodeConfiguratio=
n):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(SutNode, self).__init__(no=
de_config)<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_prefix_list =3D []<br=
>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._build_target_config =3D No=
ne<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remote_tmp_dir =3D self.ma=
in_session.get_remote_tmp_dir()<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.__remote_dpdk_dir =3D None<=
br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_version =3D None<br>
&gt;&gt; @@ -94,7 +94,7 @@ def _configure_build_target(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Populate common environment vari=
ables and set build target config.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._build_target_config =3D bu=
ild_target_config<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars.update(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.=
get_dpdk_build_env_vars(build_target_config.arch)<br>
&gt;&gt; @@ -112,7 +112,7 @@ def _copy_dpdk_tarball(self) -&gt; None:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy to and extract DPDK tarball=
 on the SUT node.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._<a href=3D"http://logger.i=
nfo" rel=3D"noreferrer" target=3D"_blank">logger.info</a>(&quot;Copying DPD=
K tarball to SUT.&quot;)<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_file(SETTINGS.=
dpdk_tarball_path, self._remote_tmp_dir)<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_to(SETTINGS.dp=
dk_tarball_path, self._remote_tmp_dir)<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # construct remote tarball path<=
br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # the basename is the same on lo=
cal host and on remote Node<br>
&gt;&gt; @@ -259,7 +259,7 @@ def run_dpdk_app(<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Run DPDK application on the remo=
te node.<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.main_session.send_co=
mmand(<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;{app_path} {eal_=
args}&quot;, timeout, verify=3DTrue<br>
&gt;&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&quot;{app_path} {eal_=
args}&quot;, timeout, privileged=3DTrue, verify=3DTrue<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt; diff --git a/dts/framework/utils.py b/dts/framework/utils.py<br>
&gt;&gt; index 55e0b0ef0e..8cfbc6a29d 100644<br>
&gt;&gt; --- a/dts/framework/utils.py<br>
&gt;&gt; +++ b/dts/framework/utils.py<br>
&gt;&gt; @@ -42,19 +42,10 @@ def expand_range(range_str: str) -&gt; list[in=
t]:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 return expanded_range<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt; -def GREEN(text: str) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 return f&quot;\u001B[32;1m{str(text)}\u001B[0m&quot=
;<br>
&gt;&gt; -<br>
&gt;&gt; -<br>
&gt;&gt;=C2=A0 def RED(text: str) -&gt; str:<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 return f&quot;\u001B[31;1m{str(text)}\u001B[0m=
&quot;<br>
&gt;&gt;<br>
&gt;&gt;<br>
&gt;&gt; -class EnvVarsDict(dict):<br>
&gt;&gt; -=C2=A0 =C2=A0 def __str__(self) -&gt; str:<br>
&gt;&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return &quot; &quot;.join([&quot;=3D&=
quot;.join(item) for item in self.items()])<br>
&gt;&gt; -<br>
&gt;&gt; -<br>
&gt;&gt;=C2=A0 class MesonArgs(object):<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 &quot;&quot;&quot;<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 Aggregate the arguments needed to build DPDK:<=
br>
&gt;&gt; diff --git a/dts/poetry.lock b/dts/poetry.lock<br>
&gt;&gt; index 0b2a007d4d..2438f337cd 100644<br>
&gt;&gt; --- a/dts/poetry.lock<br>
&gt;&gt; +++ b/dts/poetry.lock<br>
&gt;&gt; @@ -12,6 +12,18 @@ docs =3D [&quot;furo&quot;, &quot;sphinx&quot;,=
 &quot;zope.interface&quot;, &quot;sphinx-notfound-page&quot;]<br>
&gt;&gt;=C2=A0 tests =3D [&quot;coverage[toml] (&gt;=3D5.0.2)&quot;, &quot;=
hypothesis&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;zope.interface&quot;, &quot;cloudpickle&quot;]<br>
&gt;&gt;=C2=A0 tests_no_zope =3D [&quot;coverage[toml] (&gt;=3D5.0.2)&quot;=
, &quot;hypothesis&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;cloudpickle&quot;]<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;bcrypt&quot;<br>
&gt;&gt; +version =3D &quot;4.0.1&quot;<br>
&gt;&gt; +description =3D &quot;Modern password hashing for your software a=
nd your servers&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.extras]<br>
&gt;&gt; +tests =3D [&quot;pytest (&gt;=3D3.2.1,!=3D3.3.0)&quot;]<br>
&gt;&gt; +typecheck =3D [&quot;mypy&quot;]<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;black&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;22.10.0&quot;<br>
&gt;&gt; @@ -33,6 +45,17 @@ d =3D [&quot;aiohttp (&gt;=3D3.7.4)&quot;]<br>
&gt;&gt;=C2=A0 jupyter =3D [&quot;ipython (&gt;=3D7.8.0)&quot;, &quot;token=
ize-rt (&gt;=3D3.2.0)&quot;]<br>
&gt;&gt;=C2=A0 uvloop =3D [&quot;uvloop (&gt;=3D0.15.2)&quot;]<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;cffi&quot;<br>
&gt;&gt; +version =3D &quot;1.15.1&quot;<br>
&gt;&gt; +description =3D &quot;Foreign Function Interface for Python calli=
ng C code.&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.dependencies]<br>
&gt;&gt; +pycparser =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;click&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;8.1.3&quot;<br>
&gt;&gt; @@ -52,6 +75,52 @@ category =3D &quot;dev&quot;<br>
&gt;&gt;=C2=A0 optional =3D false<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D=
3.3.*,!=3D3.4.*,!=3D3.5.*,!=3D3.6.*,&gt;=3D2.7&quot;<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;cryptography&quot;<br>
&gt;&gt; +version =3D &quot;40.0.2&quot;<br>
&gt;&gt; +description =3D &quot;cryptography is a package which provides cr=
yptographic recipes and primitives to Python developers.&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.dependencies]<br>
&gt;&gt; +cffi =3D &quot;&gt;=3D1.12&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.extras]<br>
&gt;&gt; +docs =3D [&quot;sphinx (&gt;=3D5.3.0)&quot;, &quot;sphinx-rtd-the=
me (&gt;=3D1.1.1)&quot;]<br>
&gt;&gt; +docstest =3D [&quot;pyenchant (&gt;=3D1.6.11)&quot;, &quot;twine =
(&gt;=3D1.12.0)&quot;, &quot;sphinxcontrib-spelling (&gt;=3D4.0.1)&quot;]<b=
r>
&gt;&gt; +pep8test =3D [&quot;black&quot;, &quot;ruff&quot;, &quot;mypy&quo=
t;, &quot;check-manifest&quot;]<br>
&gt;&gt; +sdist =3D [&quot;setuptools-rust (&gt;=3D0.11.4)&quot;]<br>
&gt;&gt; +ssh =3D [&quot;bcrypt (&gt;=3D3.1.5)&quot;]<br>
&gt;&gt; +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;pytest-subtests&quot;, &quot;pytest-xdist&quot;, &quot;pretend&quot;,=
 &quot;iso8601&quot;]<br>
&gt;&gt; +test-randomorder =3D [&quot;pytest-randomly&quot;]<br>
&gt;&gt; +tox =3D [&quot;tox&quot;]<br>
&gt;&gt; +<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;fabric&quot;<br>
&gt;&gt; +version =3D &quot;2.7.1&quot;<br>
&gt;&gt; +description =3D &quot;High level SSH command execution&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.dependencies]<br>
&gt;&gt; +invoke =3D &quot;&gt;=3D1.3,&lt;2.0&quot;<br>
&gt;&gt; +paramiko =3D &quot;&gt;=3D2.4&quot;<br>
&gt;&gt; +pathlib2 =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.extras]<br>
&gt;&gt; +pytest =3D [&quot;mock (&gt;=3D2.0.0,&lt;3.0)&quot;, &quot;pytest=
 (&gt;=3D3.2.5,&lt;4.0)&quot;]<br>
&gt;&gt; +testing =3D [&quot;mock (&gt;=3D2.0.0,&lt;3.0)&quot;]<br>
&gt;&gt; +<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;invoke&quot;<br>
&gt;&gt; +version =3D &quot;1.7.3&quot;<br>
&gt;&gt; +description =3D &quot;Pythonic task execution&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;isort&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;5.10.1&quot;<br>
&gt;&gt; @@ -136,23 +205,41 @@ optional =3D false<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;*&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt; -name =3D &quot;pathspec&quot;<br>
&gt;&gt; -version =3D &quot;0.10.1&quot;<br>
&gt;&gt; -description =3D &quot;Utility library for gitignore style pattern=
 matching of file paths.&quot;<br>
&gt;&gt; -category =3D &quot;dev&quot;<br>
&gt;&gt; +name =3D &quot;paramiko&quot;<br>
&gt;&gt; +version =3D &quot;3.1.0&quot;<br>
&gt;&gt; +description =3D &quot;SSH2 protocol library&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt;=C2=A0 optional =3D false<br>
&gt;&gt; -python-versions =3D &quot;&gt;=3D3.7&quot;<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.dependencies]<br>
&gt;&gt; +bcrypt =3D &quot;&gt;=3D3.2&quot;<br>
&gt;&gt; +cryptography =3D &quot;&gt;=3D3.3&quot;<br>
&gt;&gt; +pynacl =3D &quot;&gt;=3D1.5&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.extras]<br>
&gt;&gt; +all =3D [&quot;pyasn1 (&gt;=3D0.1.7)&quot;, &quot;invoke (&gt;=3D=
2.0)&quot;, &quot;gssapi (&gt;=3D1.4.1)&quot;, &quot;pywin32 (&gt;=3D2.1.8)=
&quot;]<br>
&gt;&gt; +gssapi =3D [&quot;pyasn1 (&gt;=3D0.1.7)&quot;, &quot;gssapi (&gt;=
=3D1.4.1)&quot;, &quot;pywin32 (&gt;=3D2.1.8)&quot;]<br>
&gt;&gt; +invoke =3D [&quot;invoke (&gt;=3D2.0)&quot;]<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt; -name =3D &quot;pexpect&quot;<br>
&gt;&gt; -version =3D &quot;4.8.0&quot;<br>
&gt;&gt; -description =3D &quot;Pexpect allows easy control of interactive =
console applications.&quot;<br>
&gt;&gt; +name =3D &quot;pathlib2&quot;<br>
&gt;&gt; +version =3D &quot;2.3.7.post1&quot;<br>
&gt;&gt; +description =3D &quot;Object-oriented filesystem paths&quot;<br>
&gt;&gt;=C2=A0 category =3D &quot;main&quot;<br>
&gt;&gt;=C2=A0 optional =3D false<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;*&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [package.dependencies]<br>
&gt;&gt; -ptyprocess =3D &quot;&gt;=3D0.5&quot;<br>
&gt;&gt; +six =3D &quot;*&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;pathspec&quot;<br>
&gt;&gt; +version =3D &quot;0.10.1&quot;<br>
&gt;&gt; +description =3D &quot;Utility library for gitignore style pattern=
 matching of file paths.&quot;<br>
&gt;&gt; +category =3D &quot;dev&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D3.7&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;platformdirs&quot;<br>
&gt;&gt; @@ -166,14 +253,6 @@ python-versions =3D &quot;&gt;=3D3.7&quot;<br=
>
&gt;&gt;=C2=A0 docs =3D [&quot;furo (&gt;=3D2021.7.5b38)&quot;, &quot;prose=
lint (&gt;=3D0.10.2)&quot;, &quot;sphinx-autodoc-typehints (&gt;=3D1.12)&qu=
ot;, &quot;sphinx (&gt;=3D4)&quot;]<br>
&gt;&gt;=C2=A0 test =3D [&quot;appdirs (=3D=3D1.4.4)&quot;, &quot;pytest-co=
v (&gt;=3D2.7)&quot;, &quot;pytest-mock (&gt;=3D3.6)&quot;, &quot;pytest (&=
gt;=3D6)&quot;]<br>
&gt;&gt;<br>
&gt;&gt; -[[package]]<br>
&gt;&gt; -name =3D &quot;ptyprocess&quot;<br>
&gt;&gt; -version =3D &quot;0.7.0&quot;<br>
&gt;&gt; -description =3D &quot;Run a subprocess in a pseudo terminal&quot;=
<br>
&gt;&gt; -category =3D &quot;main&quot;<br>
&gt;&gt; -optional =3D false<br>
&gt;&gt; -python-versions =3D &quot;*&quot;<br>
&gt;&gt; -<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;pycodestyle&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;2.9.1&quot;<br>
&gt;&gt; @@ -182,6 +261,14 @@ category =3D &quot;dev&quot;<br>
&gt;&gt;=C2=A0 optional =3D false<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;pycparser&quot;<br>
&gt;&gt; +version =3D &quot;2.21&quot;<br>
&gt;&gt; +description =3D &quot;C parser in Python&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3=
.2.*, !=3D3.3.*&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;pydocstyle&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;6.1.1&quot;<br>
&gt;&gt; @@ -228,6 +315,21 @@ tests =3D [&quot;pytest (&gt;=3D7.1.2)&quot;,=
 &quot;pytest-mypy&quot;, &quot;eradicate (&gt;=3D2.0.0)&quot;, &quot;radon=
 (&gt;=3D5.1<br>
&gt;&gt;=C2=A0 toml =3D [&quot;toml (&gt;=3D0.10.2)&quot;]<br>
&gt;&gt;=C2=A0 vulture =3D [&quot;vulture&quot;]<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;pynacl&quot;<br>
&gt;&gt; +version =3D &quot;1.5.0&quot;<br>
&gt;&gt; +description =3D &quot;Python binding to the Networking and Crypto=
graphy (NaCl) library&quot;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.dependencies]<br>
&gt;&gt; +cffi =3D &quot;&gt;=3D1.4.1&quot;<br>
&gt;&gt; +<br>
&gt;&gt; +[package.extras]<br>
&gt;&gt; +docs =3D [&quot;sphinx (&gt;=3D1.6.5)&quot;, &quot;sphinx-rtd-the=
me&quot;]<br>
&gt;&gt; +tests =3D [&quot;pytest (&gt;=3D3.2.1,!=3D3.3.0)&quot;, &quot;hyp=
othesis (&gt;=3D3.27.0)&quot;]<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;pyrsistent&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;0.19.1&quot;<br>
&gt;&gt; @@ -244,6 +346,14 @@ category =3D &quot;main&quot;<br>
&gt;&gt;=C2=A0 optional =3D false<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;&gt;=3D3.6&quot;<br>
&gt;&gt;<br>
&gt;&gt; +[[package]]<br>
&gt;&gt; +name =3D &quot;six&quot;<br>
&gt;&gt; +version =3D &quot;1.16.0&quot;<br>
&gt;&gt; +description =3D &quot;Python 2 and 3 compatibility utilities&quot=
;<br>
&gt;&gt; +category =3D &quot;main&quot;<br>
&gt;&gt; +optional =3D false<br>
&gt;&gt; +python-versions =3D &quot;&gt;=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3=
.2.*&quot;<br>
&gt;&gt; +<br>
&gt;&gt;=C2=A0 [[package]]<br>
&gt;&gt;=C2=A0 name =3D &quot;snowballstemmer&quot;<br>
&gt;&gt;=C2=A0 version =3D &quot;2.2.0&quot;<br>
&gt;&gt; @@ -299,13 +409,18 @@ jsonschema =3D &quot;&gt;=3D4,&lt;5&quot;<br=
>
&gt;&gt;=C2=A0 [metadata]<br>
&gt;&gt;=C2=A0 lock-version =3D &quot;1.1&quot;<br>
&gt;&gt;=C2=A0 python-versions =3D &quot;^3.10&quot;<br>
&gt;&gt; -content-hash =3D &quot;a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb=
9e260a67e92c2403e2319f&quot;<br>
&gt;&gt; +content-hash =3D &quot;719c43bcaa5d181921debda884f8f714063df0b233=
6d61e9f64ecab034e8b139&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [metadata.files]<br>
&gt;&gt;=C2=A0 attrs =3D []<br>
&gt;&gt; +bcrypt =3D []<br>
&gt;&gt;=C2=A0 black =3D []<br>
&gt;&gt; +cffi =3D []<br>
&gt;&gt;=C2=A0 click =3D []<br>
&gt;&gt;=C2=A0 colorama =3D []<br>
&gt;&gt; +cryptography =3D []<br>
&gt;&gt; +fabric =3D []<br>
&gt;&gt; +invoke =3D []<br>
&gt;&gt;=C2=A0 isort =3D []<br>
&gt;&gt;=C2=A0 jsonpatch =3D []<br>
&gt;&gt;=C2=A0 jsonpointer =3D []<br>
&gt;&gt; @@ -313,22 +428,22 @@ jsonschema =3D []<br>
&gt;&gt;=C2=A0 mccabe =3D []<br>
&gt;&gt;=C2=A0 mypy =3D []<br>
&gt;&gt;=C2=A0 mypy-extensions =3D []<br>
&gt;&gt; +paramiko =3D []<br>
&gt;&gt; +pathlib2 =3D []<br>
&gt;&gt;=C2=A0 pathspec =3D []<br>
&gt;&gt; -pexpect =3D [<br>
&gt;&gt; -=C2=A0 =C2=A0 {file =3D &quot;pexpect-4.8.0-py2.py3-none-any.whl&=
quot;, hash =3D &quot;sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a1=
52b581d69ae3710937&quot;},<br>
&gt;&gt; -=C2=A0 =C2=A0 {file =3D &quot;pexpect-4.8.0.tar.gz&quot;, hash =
=3D &quot;sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6=
784c0c&quot;},<br>
&gt;&gt; -]<br>
&gt;&gt;=C2=A0 platformdirs =3D [<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 {file =3D &quot;platformdirs-2.5.2-py3-none-an=
y.whl&quot;, hash =3D &quot;sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7e=
a5d079477e722bb41ab25788&quot;},<br>
&gt;&gt;=C2=A0 =C2=A0 =C2=A0 {file =3D &quot;platformdirs-2.5.2.tar.gz&quot=
;, hash =3D &quot;sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b=
27b2a3c7feef19&quot;},<br>
&gt;&gt;=C2=A0 ]<br>
&gt;&gt; -ptyprocess =3D []<br>
&gt;&gt;=C2=A0 pycodestyle =3D []<br>
&gt;&gt; +pycparser =3D []<br>
&gt;&gt;=C2=A0 pydocstyle =3D []<br>
&gt;&gt;=C2=A0 pyflakes =3D []<br>
&gt;&gt;=C2=A0 pylama =3D []<br>
&gt;&gt; +pynacl =3D []<br>
&gt;&gt;=C2=A0 pyrsistent =3D []<br>
&gt;&gt;=C2=A0 pyyaml =3D []<br>
&gt;&gt; +six =3D []<br>
&gt;&gt;=C2=A0 snowballstemmer =3D []<br>
&gt;&gt;=C2=A0 toml =3D []<br>
&gt;&gt;=C2=A0 tomli =3D []<br>
&gt;&gt; diff --git a/dts/pyproject.toml b/dts/pyproject.toml<br>
&gt;&gt; index a136c91e5e..50bcdb327a 100644<br>
&gt;&gt; --- a/dts/pyproject.toml<br>
&gt;&gt; +++ b/dts/pyproject.toml<br>
&gt;&gt; @@ -9,10 +9,10 @@ authors =3D [&quot;Owen Hilyard &lt;<a href=3D"m=
ailto:ohilyard@iol.unh.edu" target=3D"_blank">ohilyard@iol.unh.edu</a>&gt;&=
quot;, &quot;<a href=3D"mailto:dts@dpdk.org" target=3D"_blank">dts@dpdk.org=
</a>&quot;]<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [tool.poetry.dependencies]<br>
&gt;&gt;=C2=A0 python =3D &quot;^3.10&quot;<br>
&gt;&gt; -pexpect =3D &quot;^4.8.0&quot;<br>
&gt;&gt;=C2=A0 warlock =3D &quot;^2.0.1&quot;<br>
&gt;&gt;=C2=A0 PyYAML =3D &quot;^6.0&quot;<br>
&gt;&gt;=C2=A0 types-PyYAML =3D &quot;^6.0.8&quot;<br>
&gt;&gt; +fabric =3D &quot;^2.7.1&quot;<br>
&gt;&gt;<br>
&gt;&gt;=C2=A0 [tool.poetry.dev-dependencies]<br>
&gt;&gt;=C2=A0 mypy =3D &quot;^0.961&quot;<br>
&gt;&gt; --<br>
&gt;&gt; 2.30.2<br>
&gt;&gt;<br></blockquote><div><br></div><div>Acked-by: Jeremy Spewock &lt;<=
a href=3D"mailto:jspewock@iol.unh.edu">jspewock@iol.unh.edu</a>&gt;=C2=A0</=
div></div></div>

--00000000000096d2f505facdbef9--