From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 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 ; Wed, 3 May 2023 19:54:17 +0200 (CEST) Received: by mail-pj1-f50.google.com with SMTP id 98e67ed59e1d1-2496863c2c7so5284050a91.1 for ; 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> In-Reply-To: From: Jeremy Spewock Date: Wed, 3 May 2023 13:54:05 -0400 Message-ID: Subject: Re: [PATCH v2] dts: replace pexpect with fabric To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: thomas@monjalon.net, Honnappa.Nagarahalli@arm.com, lijuan.tu@intel.com, wathsala.vithanage@arm.com, probb@iol.unh.edu, dev@dpdk.org Content-Type: multipart/alternative; boundary="00000000000096d2f505facdbef9" X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org --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 wrote: > On Fri, Apr 28, 2023 at 9:04=E2=80=AFPM Jeremy Spewock > wrote: > > > > > > > > On Mon, Apr 24, 2023 at 9:35=E2=80=AFAM Juraj Linke=C5=A1 > 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 > >> --- > >> 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 > >> > >> Running DTS > >> ----------- > >> @@ -151,7 +171,8 @@ which is a template that illustrates what can be > configured in DTS: > >> :start-at: executions: > >> > >> > >> -The user must be root or any other user with prompt starting with > ``#``. > >> +The user must have :ref:`administrator privileges ` > >> +which don't require password authentication. > >> The other fields are mostly self-explanatory > >> and documented in more detail in > ``dts/framework/config/conf_yaml_schema.json``. > >> > >> diff --git a/dts/conf.yaml b/dts/conf.yaml > >> index a9bd8a3ecf..129801d87c 100644 > >> --- a/dts/conf.yaml > >> +++ b/dts/conf.yaml > >> @@ -16,7 +16,7 @@ executions: > >> nodes: > >> - name: "SUT 1" > >> hostname: sut1.change.me.localhost > >> - user: root > >> + user: dtsuser > >> arch: x86_64 > >> os: linux > >> lcores: "" > >> diff --git a/dts/framework/exception.py b/dts/framework/exception.py > >> index ca353d98fc..44ff4e979a 100644 > >> --- a/dts/framework/exception.py > >> +++ b/dts/framework/exception.py > >> @@ -62,13 +62,19 @@ class SSHConnectionError(DTSError): > >> """ > >> > >> host: str > >> + errors: list[str] > >> severity: ClassVar[ErrorSeverity] =3D ErrorSeverity.SSH_ERR > >> > >> - def __init__(self, host: str): > >> + def __init__(self, host: str, errors: list[str] | None =3D None): > >> self.host =3D host > >> + self.errors =3D [] if errors is None else errors > >> > >> def __str__(self) -> str: > >> - return f"Error trying to connect with {self.host}" > >> + message =3D f"Error trying to connect with {self.host}." > >> + if self.errors: > >> + message +=3D f" Errors encountered while retrying: {', > '.join(self.errors)}" > >> + > >> + return message > >> > >> > >> class SSHSessionDeadError(DTSError): > >> diff --git a/dts/framework/remote_session/linux_session.py > b/dts/framework/remote_session/linux_session.py > >> index a1e3bc3a92..f13f399121 100644 > >> --- a/dts/framework/remote_session/linux_session.py > >> +++ b/dts/framework/remote_session/linux_session.py > >> @@ -14,10 +14,11 @@ class LinuxSession(PosixSession): > >> The implementation of non-Posix compliant parts of Linux remote > sessions. > >> """ > >> > >> + def _get_privileged_command(self, command: str) -> str: > >> + return f"sudo -- sh -c '{command}'" > >> + > >> def get_remote_cpus(self, use_first_core: bool) -> > list[LogicalCore]: > >> - cpu_info =3D self.remote_session.send_command( > >> - "lscpu -p=3DCPU,CORE,SOCKET,NODE|grep -v \\#" > >> - ).stdout > >> + cpu_info =3D self.send_command("lscpu > -p=3DCPU,CORE,SOCKET,NODE|grep -v \\#").stdout > >> lcores =3D [] > >> for cpu_line in cpu_info.splitlines(): > >> lcore, core, socket, node =3D map(int, cpu_line.split(","= )) > >> @@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int, > force_first_numa: bool) -> None: > >> self._mount_huge_pages() > >> > >> def _get_hugepage_size(self) -> int: > >> - hugepage_size =3D self.remote_session.send_command( > >> + hugepage_size =3D self.send_command( > >> "awk '/Hugepagesize/ {print $2}' /proc/meminfo" > >> ).stdout > >> return int(hugepage_size) > >> > >> def _get_hugepages_total(self) -> int: > >> - hugepages_total =3D self.remote_session.send_command( > >> + hugepages_total =3D self.send_command( > >> "awk '/HugePages_Total/ { print $2 }' /proc/meminfo" > >> ).stdout > >> return int(hugepages_total) > >> > >> def _get_numa_nodes(self) -> list[int]: > >> try: > >> - numa_count =3D self.remote_session.send_command( > >> + numa_count =3D self.send_command( > >> "cat /sys/devices/system/node/online", verify=3DTrue > >> ).stdout > >> numa_range =3D expand_range(numa_count) > >> @@ -70,14 +71,12 @@ def _get_numa_nodes(self) -> list[int]: > >> def _mount_huge_pages(self) -> None: > >> self._logger.info("Re-mounting Hugepages.") > >> hugapge_fs_cmd =3D "awk '/hugetlbfs/ { print $2 }' /proc/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 ", = " > 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 --00000000000096d2f505facdbef9 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable

On Tue, May 2, 2023 at 9:00=E2=80=AFAM Ju= raj Linke=C5=A1 <juraj.linkes@pantheon.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.li= nkes@pantheon.tech> wrote:
>>
>> Pexpect is not a dedicated SSH connection library while Fabric is.= With
>> Fabric, all SSH-related logic is provided and we can just focus on=
>> what's DTS specific.
>>
>> Signed-off-by: Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech>= ;
>> ---
>>=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 +-
>>=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 +-
>>=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 +-
>>=C2=A0 dts/framework/remote_session/linux_session.py |=C2=A0 31 +-<= br> >>=C2=A0 dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2= =A0 51 +++-
>>=C2=A0 dts/framework/remote_session/posix_session.py |=C2=A0 48 +--=
>>=C2=A0 .../remote_session/remote/remote_session.py=C2=A0 =C2=A0|=C2= =A0 35 ++-
>>=C2=A0 .../remote_session/remote/ssh_session.py=C2=A0 =C2=A0 =C2=A0= | 287 ++++++------------
>>=C2=A0 dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 = =C2=A0|=C2=A0 12 +-
>>=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 -
>>=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 +++= +++++--
>>=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> >>=C2=A0 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
>>
>>=C2=A0 #. **SSH Connection**
>>
>> -=C2=A0 =C2=A0DTS uses Python pexpect for SSH connections between = DTS environment and the other hosts.
>> -=C2=A0 =C2=A0The pexpect implementation is a wrapper around the s= sh command in the DTS environment.
>> -=C2=A0 =C2=A0This means it'll use the SSH agent providing the= ssh command and its keys.
>> +=C2=A0 =C2=A0DTS uses the Fabric Python library for SSH connectio= ns between DTS environment
>> +=C2=A0 =C2=A0and the other hosts.
>> +=C2=A0 =C2=A0The authentication method used is pubkey authenticat= ion.
>> +=C2=A0 =C2=A0Fabric tries to use a passed key/certificate,
>> +=C2=A0 =C2=A0then any key it can with through an SSH agent,
>> +=C2=A0 =C2=A0then any "id_rsa", "id_dsa" or &= quot;id_ecdsa" key discoverable in ``~/.ssh/``
>> +=C2=A0 =C2=A0(with any matching OpenSSH-style certificates).
>> +=C2=A0 =C2=A0DTS doesn't pass any keys, so Fabric tries to us= e the other two methods.
>>
>>
>>=C2=A0 Setting up System Under Test
>> @@ -132,6 +137,21 @@ There are two areas that need to be set up on= a System Under Test:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0It's possible to use the hugepage co= nfiguration already present on the SUT.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0If you wish to do so, don't specify = the hugepage configuration in the DTS config file.
>>
>> +#. **User with administrator privileges**
>> +
>> +.. _sut_admin_user:
>> +
>> +=C2=A0 =C2=A0DTS needs administrator privileges to run DPDK appli= cations (such as testpmd) on the SUT.
>> +=C2=A0 =C2=A0The SUT user must be able run commands in privileged= mode without asking for password.
>> +=C2=A0 =C2=A0On most Linux distributions, it's a matter of se= tting up passwordless sudo:
>> +
>> +=C2=A0 =C2=A0#. Run ``sudo visudo`` and check that it contains ``= %sudo=C2=A0 =C2=A0ALL=3D(ALL:ALL) ALL``.
>> +
>> +=C2=A0 =C2=A0#. Add the SUT user to the sudo group with:
>> +
>> +=C2=A0 =C2=A0.. code-block:: console
>> +
>> +=C2=A0 =C2=A0 =C2=A0 sudo usermod -aG sudo <sut_user>
>>
>>=C2=A0 Running DTS
>>=C2=A0 -----------
>> @@ -151,7 +171,8 @@ which is a template that illustrates what can = be configured in DTS:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0:start-at: executions:
>>
>>
>> -The user must be root or any other user with prompt starting with= ``#``.
>> +The user must have :ref:`administrator privileges <sut_admin_u= ser>`
>> +which don't require password authentication.
>>=C2=A0 The other fields are mostly self-explanatory
>>=C2=A0 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:
>>=C2=A0 nodes:
>>=C2=A0 =C2=A0 - name: "SUT 1"
>>=C2=A0 =C2=A0 =C2=A0 hostname: sut1.change.me.localhost
>> -=C2=A0 =C2=A0 user: root
>> +=C2=A0 =C2=A0 user: dtsuser
>>=C2=A0 =C2=A0 =C2=A0 arch: x86_64
>>=C2=A0 =C2=A0 =C2=A0 os: linux
>>=C2=A0 =C2=A0 =C2=A0 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):
>>=C2=A0 =C2=A0 =C2=A0 """
>>
>>=C2=A0 =C2=A0 =C2=A0 host: str
>> +=C2=A0 =C2=A0 errors: list[str]
>>=C2=A0 =C2=A0 =C2=A0 severity: ClassVar[ErrorSeverity] =3D ErrorSev= erity.SSH_ERR
>>
>> -=C2=A0 =C2=A0 def __init__(self, host: str):
>> +=C2=A0 =C2=A0 def __init__(self, host: str, errors: list[str] | N= one =3D None):
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.host =3D host
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.errors =3D [] if errors is None = else errors
>>
>>=C2=A0 =C2=A0 =C2=A0 def __str__(self) -> str:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"Error trying to connect= with {self.host}"
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 message =3D f"Error trying to co= nnect with {self.host}."
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.errors:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 message +=3D f" Er= rors encountered while retrying: {', '.join(self.errors)}"
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return message
>>
>>
>>=C2=A0 class SSHSessionDeadError(DTSError):
>> diff --git a/dts/framework/remote_session/linux_session.py b/dts/f= ramework/remote_session/linux_session.py
>> index a1e3bc3a92..f13f399121 100644
>> --- a/dts/framework/remote_session/linux_session.py
>> +++ b/dts/framework/remote_session/linux_session.py
>> @@ -14,10 +14,11 @@ class LinuxSession(PosixSession):
>>=C2=A0 =C2=A0 =C2=A0 The implementation of non-Posix compliant part= s of Linux remote sessions.
>>=C2=A0 =C2=A0 =C2=A0 """
>>
>> +=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&g= t; str:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"sudo -- sh -c '{com= mand}'"
>> +
>>=C2=A0 =C2=A0 =C2=A0 def get_remote_cpus(self, use_first_core: bool= ) -> list[LogicalCore]:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.remote_session.send= _command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "lscpu -p=3DCPU,CO= RE,SOCKET,NODE|grep -v \\#"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 cpu_info =3D self.send_command("= lscpu -p=3DCPU,CORE,SOCKET,NODE|grep -v \\#").stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 lcores =3D []
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for cpu_line in cpu_info.splitli= nes():
>>=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(","))
>> @@ -45,20 +46,20 @@ def setup_hugepages(self, hugepage_amount: int= , force_first_numa: bool) -> None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._mount_huge_pages()
>>
>>=C2=A0 =C2=A0 =C2=A0 def _get_hugepage_size(self) -> int:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.remote_session= .send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_size =3D self.send_command(<= br> >>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "awk '/Hu= gepagesize/ {print $2}' /proc/meminfo"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return int(hugepage_size)
>>
>>=C2=A0 =C2=A0 =C2=A0 def _get_hugepages_total(self) -> int:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.remote_sessi= on.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepages_total =3D self.send_command= (
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "awk '/Hu= gePages_Total/ { print $2 }' /proc/meminfo"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return int(hugepages_total)
>>
>>=C2=A0 =C2=A0 =C2=A0 def _get_numa_nodes(self) -> list[int]:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.rem= ote_session.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_count =3D self.sen= d_command(
>>=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", verify=3DTrue
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 numa_range =3D exp= and_range(numa_count)
>> @@ -70,14 +71,12 @@ def _get_numa_nodes(self) -> list[int]:
>>=C2=A0 =C2=A0 =C2=A0 def _mount_huge_pages(self) -> None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info("Re-mounting= Hugepages.")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugapge_fs_cmd =3D "awk = 9;/hugetlbfs/ { print $2 }' /proc/mounts"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&qu= ot;umount $({hugapge_fs_cmd})")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c= ommand(hugapge_fs_cmd)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"umount $({hu= gapge_fs_cmd})")
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(hugapge_= fs_cmd)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if result.stdout =3D=3D "&q= uot;:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_mount_path = =3D "/mnt/huge"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen= d_command(f"mkdir -p {remote_mount_path}")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen= d_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"mo= unt -t hugetlbfs nodev {remote_mount_path}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo= t;mkdir -p {remote_mount_path}")
>> +=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}")
>>
>>=C2=A0 =C2=A0 =C2=A0 def _supports_numa(self) -> bool:
>>=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
>> @@ -94,14 +93,12 @@ def _configure_huge_pages(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if force_first_numa and self._su= pports_numa():
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # clear non-numa h= ugepages
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen= d_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"ec= ho 0 | sudo tee {hugepage_config_path}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo= t;echo 0 | tee {hugepage_config_path}", privileged=3DTrue)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_config_pa= th =3D (
>>=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"
>>=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"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"echo {amount} | = sudo tee {hugepage_config_path}"
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"echo {amount} | = tee {hugepage_config_path}", privileged=3DTrue
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> diff --git a/dts/framework/remote_session/os_session.py b/dts/fram= ework/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 @@
>>=C2=A0 from framework.logger import DTSLOG
>>=C2=A0 from framework.settings import SETTINGS
>>=C2=A0 from framework.testbed_model import LogicalCore
>> -from framework.utils import EnvVarsDict, MesonArgs
>> +from framework.utils import MesonArgs
>>
>>=C2=A0 from .remote import CommandResult, RemoteSession, create_rem= ote_session
>>
>> @@ -53,17 +53,32 @@ def is_alive(self) -> bool:
>>=C2=A0 =C2=A0 =C2=A0 def send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: str,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged: bool =3D False,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify: bool =3D False,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,
>>=C2=A0 =C2=A0 =C2=A0 ) -> CommandResult:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 An all-purpose API in case the c= ommand to be executed is already
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 OS-agnostic, such as when the pa= th to the executed command has been
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 constructed beforehand.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if privileged:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D self._get_p= rivileged_command(command)
>> +
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.remote_session.send_= command(command, timeout, verify, env)
>>
>> +=C2=A0 =C2=A0 @abstractmethod
>> +=C2=A0 =C2=A0 def _get_privileged_command(self, command: str) -&g= t; str:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Modify the command = so that it executes with administrative privileges.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to= modify.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 The modified command th= at executes with administrative privileges.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> +
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>>=C2=A0 =C2=A0 =C2=A0 def guess_dpdk_remote_dir(self, remote_dir) -&= gt; PurePath:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> @@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PureP= ath) -> PurePath:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>> -=C2=A0 =C2=A0 def copy_file(
>> +=C2=A0 =C2=A0 def copy_from(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath= ,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from th= e remote Node to the local filesystem.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node= associated with this remote
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the lo= cal filesystem.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o= n the remote Node.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil= e or directory path on the local filesystem.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> +
>> +=C2=A0 =C2=A0 @abstractmethod
>> +=C2=A0 =C2=A0 def copy_to(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
>> +=C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from lo= cal filesystem to the remote Node.
>> +
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local file= system to destination_file
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th= e remote session.
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the= direction - copy source_file from the
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated remote Node to destination= _file on local storage.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th= is remote session.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o= n the local filesystem.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil= e or directory path on the remote Node.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>> @@ -128,7 +161,7 @@ def extract_remote_tarball(
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>>=C2=A0 =C2=A0 =C2=A0 def build_dpdk(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 meson_args: MesonArgs,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_dir: str | PurePath,=
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_build_dir: str | Pur= ePath,
>> diff --git a/dts/framework/remote_session/posix_session.py b/dts/f= ramework/remote_session/posix_session.py
>> index d38062e8d6..8ca0acb429 100644
>> --- a/dts/framework/remote_session/posix_session.py
>> +++ b/dts/framework/remote_session/posix_session.py
>> @@ -9,7 +9,7 @@
>>=C2=A0 from framework.config import Architecture
>>=C2=A0 from framework.exception import DPDKBuildError, RemoteComman= dExecutionError
>>=C2=A0 from framework.settings import SETTINGS
>> -from framework.utils import EnvVarsDict, MesonArgs
>> +from framework.utils import MesonArgs
>>
>>=C2=A0 from .os_session import OSSession
>>
>> @@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -> st= r:
>>
>>=C2=A0 =C2=A0 =C2=A0 def guess_dpdk_remote_dir(self, remote_dir) -&= gt; PurePosixPath:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_guess =3D self.join_remot= e_path(remote_dir, "dpdk-*")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c= ommand(f"ls -d {remote_guess} | tail -1")
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f"l= s -d {remote_guess} | tail -1")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return PurePosixPath(result.stdo= ut)
>>
>>=C2=A0 =C2=A0 =C2=A0 def get_remote_tmp_dir(self) -> PurePosixPa= th:
>> @@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architec= ture) -> dict:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars =3D {}
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if arch =3D=3D Architecture.i686= :
>>=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
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_ses= sion.send_command("find /usr -type d -name pkgconfig")
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_comma= nd("find /usr -type d -name pkgconfig")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pkg_path =3D "= ;"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 res_path =3D out.s= tdout.split("\r\n")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for cur_path in re= s_path:
>> @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Archit= ecture) -> dict:
>>=C2=A0 =C2=A0 =C2=A0 def join_remote_path(self, *args: str | PurePa= th) -> PurePosixPath:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return PurePosixPath(*args)
>>
>> -=C2=A0 =C2=A0 def copy_file(
>> +=C2=A0 =C2=A0 def copy_from(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath= ,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_file(source_= file, destination_file, source_remote)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_from(source_= file, destination_file)
>> +
>> +=C2=A0 =C2=A0 def copy_to(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
>> +=C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.copy_to(source_fi= le, destination_file)
>>
>>=C2=A0 =C2=A0 =C2=A0 def remove_remote_dir(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> @@ -80,24 +86,24 @@ def remove_remote_dir(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 force: bool =3D True,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 opts =3D PosixSession.combine_sh= ort_options(r=3Drecursive, f=3Dforce)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(f&qu= ot;rm{opts} {remote_dir_path}")
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f"rm{opts} {re= mote_dir_path}")
>>
>>=C2=A0 =C2=A0 =C2=A0 def extract_remote_tarball(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_tarball_path: str | PureP= ath,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 expected_dir: str | PurePath | N= one =3D None,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"tar xfm {re= mote_tarball_path} "
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"-C {PurePos= ixPath(remote_tarball_path).parent}",
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 60,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if expected_dir:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen= d_command(f"ls {expected_dir}", verify=3DTrue)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(f&quo= t;ls {expected_dir}", verify=3DTrue)
>>
>>=C2=A0 =C2=A0 =C2=A0 def build_dpdk(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: EnvVarsDict,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env_vars: dict,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 meson_args: MesonArgs,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_dir: str | PurePath,=
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 remote_dpdk_build_dir: str | Pur= ePath,
>> @@ -108,7 +114,7 @@ def build_dpdk(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if rebuild:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # re= configure, then build
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= ._logge= r.info("Reconfiguring DPDK build.")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo= te_session.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send= _command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"meson configure {meson_args} {remote_dpdk_build_dir}"= ;,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 timeout,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 verify=3DTrue,
>> @@ -118,7 +124,7 @@ def build_dpdk(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # fr= esh build - remove target dir first, then build from scratch
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self= ._logge= r.info("Configuring DPDK build from scratch.")
>>=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)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo= te_session.send_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send= _command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"meson setup "
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 f"{meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}&qu= ot;,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 timeout,
>> @@ -127,14 +133,14 @@ def build_dpdk(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ) >>
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(&qu= ot;Building DPDK.")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remote_session.sen= d_command(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f&qu= ot;ninja -C {remote_dpdk_build_dir}", timeout, verify=3DTrue, env=3Den= v_vars
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except RemoteCommandExecutionErr= or as e:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise DPDKBuildErr= or(f"DPDK build failed when doing '{e.command}'.")
>>
>>=C2=A0 =C2=A0 =C2=A0 def get_dpdk_version(self, build_dir: str | Pu= rePath) -> str:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_comm= and(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"cat {self.j= oin_remote_path(build_dir, 'VERSION')}", verify=3DTrue
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return out.stdout
>> @@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_l= ist: Iterable[str]) -> None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # kill and cleanup= only if DPDK is running
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_pids =3D self= ._get_dpdk_pids(dpdk_runtime_dirs)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_pid in dp= dk_pids:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.remo= te_session.send_command(f"kill -9 {dpdk_pid}", 20)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send= _command(f"kill -9 {dpdk_pid}", 20)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._check_dpdk_h= ugepages(dpdk_runtime_dirs)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remove_dpdk_= runtime_dirs(dpdk_runtime_dirs)
>>
>> @@ -168,7 +174,7 @@ def _list_remote_dirs(self, remote_path: str |= PurePath) -> list[str] | None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Return a list of directories of = the remote_dir.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 If remote_path doesn't exist= , return None.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.remote_session.send_comm= and(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D self.send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"ls -l {remo= te_path} | awk '/^d/ {{print $NF}}'"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if "No such file or directo= ry" in out:
>> @@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: It= erable[str | PurePath]) -> list[in
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_run= time_dirs:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 dpdk_config_file = =3D PurePosixPath(dpdk_runtime_dir, "config")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_fi= les_exists(dpdk_config_file):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s= elf.remote_session.send_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"lsof -Fp {dpdk_config_file}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s= elf.send_command(f"lsof -Fp {dpdk_config_file}").stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if o= ut and "No such file or directory" not in out:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 for out_line in out.splitlines():
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 match =3D re.match(pid_regex, out_line)
>> @@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: It= erable[str | PurePath]) -> list[in
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return pids
>>
>>=C2=A0 =C2=A0 =C2=A0 def _remote_files_exists(self, remote_path: Pu= rePath) -> bool:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.remote_session.send_c= ommand(f"test -e {remote_path}")
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 result =3D self.send_command(f"t= est -e {remote_path}")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return not result.return_code >>
>>=C2=A0 =C2=A0 =C2=A0 def _check_dpdk_hugepages(
>> @@ -202,9 +206,7 @@ def _check_dpdk_hugepages(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for dpdk_runtime_dir in dpdk_run= time_dirs:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 hugepage_info =3D = PurePosixPath(dpdk_runtime_dir, "hugepage_info")
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._remote_fi= les_exists(hugepage_info):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s= elf.remote_session.send_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"lsof -Fp {hugepage_info}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ).stdout<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out =3D s= elf.send_command(f"lsof -Fp {hugepage_info}").stdout
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if o= ut and "No such file or directory" not in out:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 self._logger.warning("Some DPDK processes did not free huge= pages.")
>>=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("*************************************= ******")
>> 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 @@
>>=C2=A0 from framework.exception import RemoteCommandExecutionError<= br> >>=C2=A0 from framework.logger import DTSLOG
>>=C2=A0 from framework.settings import SETTINGS
>> -from framework.utils import EnvVarsDict
>>
>>
>>=C2=A0 @dataclasses.dataclass(slots=3DTrue, frozen=3DTrue)
>> @@ -89,7 +88,7 @@ def send_command(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: str,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.time= out,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 verify: bool =3D False,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: EnvVarsDict | None =3D None,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 env: dict | None =3D None,
>>=C2=A0 =C2=A0 =C2=A0 ) -> CommandResult:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a command to the connected = node using optional env vars
>> @@ -114,7 +113,7 @@ def send_command(
>>
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>>=C2=A0 =C2=A0 =C2=A0 def _send_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e= nv: EnvVarsDict | None
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e= nv: dict | None
>>=C2=A0 =C2=A0 =C2=A0 ) -> CommandResult:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Use the underlying protocol to e= xecute the command using optional env vars
>> @@ -141,15 +140,33 @@ def is_alive(self) -> bool:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>
>>=C2=A0 =C2=A0 =C2=A0 @abstractmethod
>> -=C2=A0 =C2=A0 def copy_file(
>> +=C2=A0 =C2=A0 def copy_from(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath= ,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from th= e remote Node to the local filesystem.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from the remote Node= associated with this remote
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session to destination_file on the lo= cal filesystem.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o= n the remote Node.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil= e or directory path on the local filesystem.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesyste= m to destination_file on the remote Node
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated with the remote session. >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 If source_remote is True, reverse the= direction - copy source_file from the
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 associated Node to destination_file o= n local filesystem.
>> +
>> +=C2=A0 =C2=A0 @abstractmethod
>> +=C2=A0 =C2=A0 def copy_to(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
>> +=C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Copy a file from lo= cal filesystem to the remote Node.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy source_file from local filesyste= m to destination_file
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 on the remote Node associated with th= is remote session.
>> +
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: the file o= n the local filesystem.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: a fil= e or directory path on the remote Node.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> diff --git a/dts/framework/remote_session/remote/ssh_session.py b/= dts/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 @@
>>=C2=A0 # SPDX-License-Identifier: BSD-3-Clause
>> -# Copyright(c) 2010-2014 Intel Corporation
>> -# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
>> -# Copyright(c) 2022-2023 University of New Hampshire
>> +# Copyright(c) 2023 PANTHEON.tech s.r.o.
>>
>
> I've noticed in other patches you'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?
>

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 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.
=C2=A0
>>
>> -import time
>> +import socket
>> +import traceback
>>=C2=A0 from pathlib import PurePath
>>
>> -import pexpect=C2=A0 # type: ignore
>> -from pexpect import pxssh=C2=A0 # type: ignore
>> +from fabric import Connection=C2=A0 # type: ignore[import]
>> +from invoke.exceptions import (=C2=A0 # type: ignore[import]
>> +=C2=A0 =C2=A0 CommandTimedOut,
>> +=C2=A0 =C2=A0 ThreadException,
>> +=C2=A0 =C2=A0 UnexpectedExit,
>> +)
>> +from paramiko.ssh_exception import (=C2=A0 # type: ignore[import]=
>> +=C2=A0 =C2=A0 AuthenticationException,
>> +=C2=A0 =C2=A0 BadHostKeyException,
>> +=C2=A0 =C2=A0 NoValidConnectionsError,
>> +=C2=A0 =C2=A0 SSHException,
>> +)
>>
>>=C2=A0 from framework.config import NodeConfiguration
>>=C2=A0 from framework.exception import SSHConnectionError, SSHSessi= onDeadError, SSHTimeoutError
>>=C2=A0 from framework.logger import DTSLOG
>> -from framework.utils import GREEN, RED, EnvVarsDict
>>
>>=C2=A0 from .remote_session import CommandResult, RemoteSession
>>
>>
>>=C2=A0 class SSHSession(RemoteSession):
>> -=C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 Module for creating Pexpect SSH remote sessions. >> +=C2=A0 =C2=A0 """A persistent SSH connection to a = remote Node.
>> +
>> +=C2=A0 =C2=A0 The connection is implemented with the Fabric Pytho= n library.
>> +
>> +=C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 node_config: The configuration of the= Node to connect to.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: The name of the session= .
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: The logger used for logging.<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 This should be passed f= rom the parent OSSession.
>> +
>> +=C2=A0 =C2=A0 Attributes:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 session: The underlying Fabric SSH co= nnection.
>> +
>> +=C2=A0 =C2=A0 Raises:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHConnectionError: The connection ca= nnot be established.
>>=C2=A0 =C2=A0 =C2=A0 """
>>
>> -=C2=A0 =C2=A0 session: pxssh.pxssh
>> -=C2=A0 =C2=A0 magic_prompt: str
>> +=C2=A0 =C2=A0 session: Connection
>>
>>=C2=A0 =C2=A0 =C2=A0 def __init__(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> @@ -31,218 +51,91 @@ def __init__(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 session_name: str,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: DTSLOG,
>>=C2=A0 =C2=A0 =C2=A0 ):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.magic_prompt =3D "MAGIC PRO= MPT"
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(SSHSession, self).__init__= (node_config, session_name, logger)
>>
>>=C2=A0 =C2=A0 =C2=A0 def _connect(self) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Create connection to assigned node. >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 errors =3D []
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 retry_attempts =3D 10
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 login_timeout =3D 20 if self.por= t else 10
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 password_regex =3D (
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 r"(?i)(?:password:= )|(?:passphrase for key)|(?i)(password for .+:)"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in ra= nge(retry_attempts):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess= ion =3D pxssh.pxssh(encoding=3D"utf-8")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 self.session.login(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 self.ip,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 self.username,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 self.password,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 original_prompt=3D"[$#>]",
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 port=3Dself.port,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 login_timeout=3Dlogin_timeout,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 password_regex=3Dpassword_regex,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 break
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Ex= ception as e:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 self._logger.warning(e)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 time.sleep(2)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 self._logger.info(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 f"Retrying connection: retry number {retry_attemp= t + 1}."
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise Exc= eption(f"Connection to {self.hostname} failed")
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("= stty -echo", "#")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("= stty columns 1000", "#")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_expect("= bind 'set enable-bracketed-paste off'", "#")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(RED(= str(e)))
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if getattr(self, "= port", None):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 suggestio= n =3D (
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"\nSuggestion: Check if the firewall on {self.hostname} is &qu= ot;
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"stopped.\n"
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for retry_attempt in range(retry_atte= mpts):
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess= ion =3D Connection(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 self.ip,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 user=3Dself.username,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 port=3Dself.port,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 connect_kwargs=3D{"password": self.password},
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 connect_timeout=3Dlogin_timeout,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ) >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.inf= o(GREEN(suggestion))
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionErro= r(self.hostname)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess= ion.open()
>>
>> -=C2=A0 =C2=A0 def send_expect(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, prompt: str, time= out: float =3D 15, verify: bool =3D False
>> -=C2=A0 =C2=A0 ) -> str | int:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret =3D self.send_expec= t_base(command, prompt, timeout)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if verify:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ret_statu= s =3D self.send_expect_base("echo $?", prompt, timeout)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 retval =3D int(ret_status)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 if retval:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 self._logger.error(f"Command: {command} failure!&= quot;)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 self._logger.error(ret)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 return retval
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 return ret
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except Va= lueError:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 return ret
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return re= t
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"Ex= ception happened in [{command}] and output is "
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"[{= self._get_output()}]"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e
>> -
>> -=C2=A0 =C2=A0 def send_expect_base(self, command: str, prompt: st= r, timeout: float) -> str:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session()
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 original_prompt =3D self.session.PROM= PT
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D prompt
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._prompt(command, timeout)
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D original_prom= pt
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
>> -
>> -=C2=A0 =C2=A0 def _clean_session(self) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.magic_pr= ompt
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.get_output(timeout=3D0.01)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.= UNIQUE_PROMPT
>> -
>> -=C2=A0 =C2=A0 def _send_line(self, command: str) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr= or(self.hostname)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if len(command) =3D=3D 2 and command.= startswith("^"):
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendcontro= l(command[1])
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.sendline(c= ommand)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (ValueError, Bad= HostKeyException, AuthenticationException) as e:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log= ger.exception(e)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSH= ConnectionError(self.hostname) from e
>>
>> -=C2=A0 =C2=A0 def _prompt(self, command: str, timeout: float) -&g= t; None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.session.prompt(timeout):<= br> >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(c= ommand, self._get_output()) from None
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 except (NoValidConnecti= onsError, socket.error, SSHException) as e:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log= ger.debug(traceback.format_exc())
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._log= ger.warning(e)
>>
>> -=C2=A0 =C2=A0 def get_output(self, timeout: float =3D 15) -> s= tr:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Get all output before timeout
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(tim= eout)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 pass
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self._get_output()
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._flush()
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 error =3D= repr(e)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if error = not in errors:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 errors.append(error)
>>
>> -=C2=A0 =C2=A0 def _get_output(self) -> str:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if not self.is_alive():
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr= or(self.hostname)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 before =3D self.session.before.rsplit= ("\r\n", 1)[0]
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if before =3D=3D "[PEXPECT]"= ;:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return ""
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return before
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.inf= o(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 f"Retrying connection: retry number {retry_attempt + 1}."<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>
>> -=C2=A0 =C2=A0 def _flush(self) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Clear all session buffer
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.buffer =3D ""<= br> >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.before =3D ""<= br> >> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHConnectionErro= r(self.hostname, errors)
>>
>>=C2=A0 =C2=A0 =C2=A0 def is_alive(self) -> bool:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.isalive()
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.session.is_connected
>>
>>=C2=A0 =C2=A0 =C2=A0 def _send_command(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e= nv: EnvVarsDict | None
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e= nv: dict | None
>>=C2=A0 =C2=A0 =C2=A0 ) -> CommandResult:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self._send_command_get_out= put(command, timeout, env)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return_code =3D int(self._send_comman= d_get_output("echo $?", timeout, None))
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send a command and = return the result of the execution.
>>
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 # we're capturing only stdout
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(self.name, command, o= utput, "", return_code)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Args:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command: The command to= execute.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: Wait at most t= his many seconds for the execution to complete.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 env: Extra environment = variables that will be used in command execution.
>>
>> -=C2=A0 =C2=A0 def _send_command_get_output(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self, command: str, timeout: float, e= nv: EnvVarsDict | None
>> -=C2=A0 =C2=A0 ) -> str:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Raises:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHSessionDeadError: Th= e session died while executing the command.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 SSHTimeoutError: The co= mmand execution timed out.
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 try:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._clean_session() >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if env:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command = =3D f"{env} {command}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._send_line(command= )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 except Exception as e:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise e
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.session= .run(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command, = env=3Denv, warn=3DTrue, hide=3DTrue, timeout=3Dtimeout
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 output =3D self.get_output(timeout=3D= timeout)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.PROMPT =3D self.session.= UNIQUE_PROMPT
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.prompt(0.1)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except (UnexpectedExit, ThreadExcepti= on) as e:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(= e)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHSessionDeadErr= or(self.hostname) from e
>>
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return output
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 except CommandTimedOut as e:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.exception(= e)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 raise SSHTimeoutError(c= ommand, e.result.stderr) from e
>>
>> -=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -> None:=
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if force is True:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close() >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.is_alive():
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.sess= ion.logout()
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return CommandResult(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.name, command, output.s= tdout, output.stderr, output.return_code
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>
>> -=C2=A0 =C2=A0 def copy_file(
>> +=C2=A0 =C2=A0 def copy_from(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath= ,
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_remote: bool =3D False,
>>=C2=A0 =C2=A0 =C2=A0 ) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Send a local file to a remote host. >> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if source_remote:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file =3D f"= {self.username}@{self.ip}:{source_file}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file =3D f&= quot;{self.username}@{self.ip}:{destination_file}"
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.get(str(destination_file= ), str(source_file))
>>
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D ""
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self.port:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 port =3D f" -P {se= lf.port}"
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 command =3D (
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"scp -v{port} -o = NoHostAuthenticationForLocalhost=3Dyes"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f" {source_file} {= destination_file}"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._spawn_scp(command)
>> +=C2=A0 =C2=A0 def copy_to(
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 source_file: str | PurePath,
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 destination_file: str | PurePath,
>> +=C2=A0 =C2=A0 ) -> None:
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.put(str(source_file), st= r(destination_file))
>>
>> -=C2=A0 =C2=A0 def _spawn_scp(self, scp_cmd: str) -> None:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 Transfer a file with SCP
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(scp_cmd)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 p: pexpect.spawn =3D pexpect.spawn(sc= p_cmd)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 ssh_newkey: str =3D "Are you sur= e you want to continue connecting"
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 i: int =3D p.expect(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 [ssh_newkey, "[pP]= assword", "# ", pexpect.EOF, pexpect.TIMEOUT], 120
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 0:=C2=A0 # add once in tr= ust list
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline("yes&qu= ot;)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 i =3D p.expect([ssh_new= key, "[pP]assword", pexpect.EOF], 2)
>> -
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 1:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 time.sleep(0.5)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.sendline(self.passwor= d)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 p.expect("Exit sta= tus 0", 60)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 if i =3D=3D 4:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.error(&quo= t;SCP TIMEOUT error %d" % i)
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 p.close()
>> +=C2=A0 =C2=A0 def _close(self, force: bool =3D False) -> None:=
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.session.close()
>> diff --git a/dts/framework/testbed_model/sut_node.py b/dts/framewo= rk/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 @@
>>=C2=A0 from framework.config import BuildTargetConfiguration, NodeC= onfiguration
>>=C2=A0 from framework.remote_session import CommandResult, OSSessio= n
>>=C2=A0 from framework.settings import SETTINGS
>> -from framework.utils import EnvVarsDict, MesonArgs
>> +from framework.utils import MesonArgs
>>
>>=C2=A0 from .hw import LogicalCoreCount, LogicalCoreList, VirtualDe= vice
>>=C2=A0 from .node import Node
>> @@ -27,7 +27,7 @@ class SutNode(Node):
>>=C2=A0 =C2=A0 =C2=A0 _dpdk_prefix_list: list[str]
>>=C2=A0 =C2=A0 =C2=A0 _dpdk_timestamp: str
>>=C2=A0 =C2=A0 =C2=A0 _build_target_config: BuildTargetConfiguration= | None
>> -=C2=A0 =C2=A0 _env_vars: EnvVarsDict
>> +=C2=A0 =C2=A0 _env_vars: dict
>>=C2=A0 =C2=A0 =C2=A0 _remote_tmp_dir: PurePath
>>=C2=A0 =C2=A0 =C2=A0 __remote_dpdk_dir: PurePath | None
>>=C2=A0 =C2=A0 =C2=A0 _dpdk_version: str | None
>> @@ -38,7 +38,7 @@ def __init__(self, node_config: NodeConfiguratio= n):
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 super(SutNode, self).__init__(no= de_config)
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_prefix_list =3D [] >>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._build_target_config =3D No= ne
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._remote_tmp_dir =3D self.ma= in_session.get_remote_tmp_dir()
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.__remote_dpdk_dir =3D None<= br> >>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._dpdk_version =3D None
>> @@ -94,7 +94,7 @@ def _configure_build_target(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Populate common environment vari= ables and set build target config.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D EnvVarsDict()
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars =3D {}
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._build_target_config =3D bu= ild_target_config
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._env_vars.update(
>>=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)
>> @@ -112,7 +112,7 @@ def _copy_dpdk_tarball(self) -> None:
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Copy to and extract DPDK tarball= on the SUT node.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info("Copying DPD= K tarball to SUT.")
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_file(SETTINGS.= dpdk_tarball_path, self._remote_tmp_dir)
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.main_session.copy_to(SETTINGS.dp= dk_tarball_path, self._remote_tmp_dir)
>>
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # construct remote tarball path<= br> >>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # the basename is the same on lo= cal host and on remote Node
>> @@ -259,7 +259,7 @@ def run_dpdk_app(
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 Run DPDK application on the remo= te node.
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 return self.main_session.send_co= mmand(
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{app_path} {eal_= args}", timeout, verify=3DTrue
>> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 f"{app_path} {eal_= args}", timeout, privileged=3DTrue, verify=3DTrue
>>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 )
>>
>>
>> diff --git a/dts/framework/utils.py b/dts/framework/utils.py
>> index 55e0b0ef0e..8cfbc6a29d 100644
>> --- a/dts/framework/utils.py
>> +++ b/dts/framework/utils.py
>> @@ -42,19 +42,10 @@ def expand_range(range_str: str) -> list[in= t]:
>>=C2=A0 =C2=A0 =C2=A0 return expanded_range
>>
>>
>> -def GREEN(text: str) -> str:
>> -=C2=A0 =C2=A0 return f"\u001B[32;1m{str(text)}\u001B[0m"= ;
>> -
>> -
>>=C2=A0 def RED(text: str) -> str:
>>=C2=A0 =C2=A0 =C2=A0 return f"\u001B[31;1m{str(text)}\u001B[0m= "
>>
>>
>> -class EnvVarsDict(dict):
>> -=C2=A0 =C2=A0 def __str__(self) -> str:
>> -=C2=A0 =C2=A0 =C2=A0 =C2=A0 return " ".join(["=3D&= quot;.join(item) for item in self.items()])
>> -
>> -
>>=C2=A0 class MesonArgs(object):
>>=C2=A0 =C2=A0 =C2=A0 """
>>=C2=A0 =C2=A0 =C2=A0 Aggregate the arguments needed to build DPDK:<= br> >> 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"]
>>=C2=A0 tests =3D ["coverage[toml] (>=3D5.0.2)", "= hypothesis", "pympler", "pytest (>=3D4.3.0)", &= quot;mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins", = "zope.interface", "cloudpickle"]
>>=C2=A0 tests_no_zope =3D ["coverage[toml] (>=3D5.0.2)"= , "hypothesis", "pympler", "pytest (>=3D4.3.0)&= quot;, "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 a= nd 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"]
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "black"
>>=C2=A0 version =3D "22.10.0"
>> @@ -33,6 +45,17 @@ d =3D ["aiohttp (>=3D3.7.4)"]
>>=C2=A0 jupyter =3D ["ipython (>=3D7.8.0)", "token= ize-rt (>=3D3.2.0)"]
>>=C2=A0 uvloop =3D ["uvloop (>=3D0.15.2)"]
>>
>> +[[package]]
>> +name =3D "cffi"
>> +version =3D "1.15.1"
>> +description =3D "Foreign Function Interface for Python calli= ng C code."
>> +category =3D "main"
>> +optional =3D false
>> +python-versions =3D "*"
>> +
>> +[package.dependencies]
>> +pycparser =3D "*"
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "click"
>>=C2=A0 version =3D "8.1.3"
>> @@ -52,6 +75,52 @@ category =3D "dev"
>>=C2=A0 optional =3D false
>>=C2=A0 python-versions =3D "!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D= 3.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 cr= yptographic 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-the= me (>=3D1.1.1)"]
>> +docstest =3D ["pyenchant (>=3D1.6.11)", "twine = (>=3D1.12.0)", "sphinxcontrib-spelling (>=3D4.0.1)"] >> +pep8test =3D ["black", "ruff", "mypy&quo= t;, "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 "*"
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "isort"
>>=C2=A0 version =3D "5.10.1"
>> @@ -136,23 +205,41 @@ optional =3D false
>>=C2=A0 python-versions =3D "*"
>>
>>=C2=A0 [[package]]
>> -name =3D "pathspec"
>> -version =3D "0.10.1"
>> -description =3D "Utility library for gitignore style pattern= matching of file paths."
>> -category =3D "dev"
>> +name =3D "paramiko"
>> +version =3D "3.1.0"
>> +description =3D "SSH2 protocol library"
>> +category =3D "main"
>>=C2=A0 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 (>=3D= 2.0)", "gssapi (>=3D1.4.1)", "pywin32 (>=3D2.1.8)= "]
>> +gssapi =3D ["pyasn1 (>=3D0.1.7)", "gssapi (>= =3D1.4.1)", "pywin32 (>=3D2.1.8)"]
>> +invoke =3D ["invoke (>=3D2.0)"]
>>
>>=C2=A0 [[package]]
>> -name =3D "pexpect"
>> -version =3D "4.8.0"
>> -description =3D "Pexpect allows easy control of interactive = console applications."
>> +name =3D "pathlib2"
>> +version =3D "2.3.7.post1"
>> +description =3D "Object-oriented filesystem paths"
>>=C2=A0 category =3D "main"
>>=C2=A0 optional =3D false
>>=C2=A0 python-versions =3D "*"
>>
>>=C2=A0 [package.dependencies]
>> -ptyprocess =3D ">=3D0.5"
>> +six =3D "*"
>> +
>> +[[package]]
>> +name =3D "pathspec"
>> +version =3D "0.10.1"
>> +description =3D "Utility library for gitignore style pattern= matching of file paths."
>> +category =3D "dev"
>> +optional =3D false
>> +python-versions =3D ">=3D3.7"
>>
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "platformdirs"
>> @@ -166,14 +253,6 @@ python-versions =3D ">=3D3.7" >>=C2=A0 docs =3D ["furo (>=3D2021.7.5b38)", "prose= lint (>=3D0.10.2)", "sphinx-autodoc-typehints (>=3D1.12)&qu= ot;, "sphinx (>=3D4)"]
>>=C2=A0 test =3D ["appdirs (=3D=3D1.4.4)", "pytest-co= v (>=3D2.7)", "pytest-mock (>=3D3.6)", "pytest (&= gt;=3D6)"]
>>
>> -[[package]]
>> -name =3D "ptyprocess"
>> -version =3D "0.7.0"
>> -description =3D "Run a subprocess in a pseudo terminal"=
>> -category =3D "main"
>> -optional =3D false
>> -python-versions =3D "*"
>> -
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "pycodestyle"
>>=C2=A0 version =3D "2.9.1"
>> @@ -182,6 +261,14 @@ category =3D "dev"
>>=C2=A0 optional =3D false
>>=C2=A0 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.*"
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "pydocstyle"
>>=C2=A0 version =3D "6.1.1"
>> @@ -228,6 +315,21 @@ tests =3D ["pytest (>=3D7.1.2)",= "pytest-mypy", "eradicate (>=3D2.0.0)", "radon= (>=3D5.1
>>=C2=A0 toml =3D ["toml (>=3D0.10.2)"]
>>=C2=A0 vulture =3D ["vulture"]
>>
>> +[[package]]
>> +name =3D "pynacl"
>> +version =3D "1.5.0"
>> +description =3D "Python binding to the Networking and Crypto= graphy (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-the= me"]
>> +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)", "hyp= othesis (>=3D3.27.0)"]
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "pyrsistent"
>>=C2=A0 version =3D "0.19.1"
>> @@ -244,6 +346,14 @@ category =3D "main"
>>=C2=A0 optional =3D false
>>=C2=A0 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.*"
>> +
>>=C2=A0 [[package]]
>>=C2=A0 name =3D "snowballstemmer"
>>=C2=A0 version =3D "2.2.0"
>> @@ -299,13 +409,18 @@ jsonschema =3D ">=3D4,<5" >>=C2=A0 [metadata]
>>=C2=A0 lock-version =3D "1.1"
>>=C2=A0 python-versions =3D "^3.10"
>> -content-hash =3D "a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb= 9e260a67e92c2403e2319f"
>> +content-hash =3D "719c43bcaa5d181921debda884f8f714063df0b233= 6d61e9f64ecab034e8b139"
>>
>>=C2=A0 [metadata.files]
>>=C2=A0 attrs =3D []
>> +bcrypt =3D []
>>=C2=A0 black =3D []
>> +cffi =3D []
>>=C2=A0 click =3D []
>>=C2=A0 colorama =3D []
>> +cryptography =3D []
>> +fabric =3D []
>> +invoke =3D []
>>=C2=A0 isort =3D []
>>=C2=A0 jsonpatch =3D []
>>=C2=A0 jsonpointer =3D []
>> @@ -313,22 +428,22 @@ jsonschema =3D []
>>=C2=A0 mccabe =3D []
>>=C2=A0 mypy =3D []
>>=C2=A0 mypy-extensions =3D []
>> +paramiko =3D []
>> +pathlib2 =3D []
>>=C2=A0 pathspec =3D []
>> -pexpect =3D [
>> -=C2=A0 =C2=A0 {file =3D "pexpect-4.8.0-py2.py3-none-any.whl&= quot;, hash =3D "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a1= 52b581d69ae3710937"},
>> -=C2=A0 =C2=A0 {file =3D "pexpect-4.8.0.tar.gz", hash = =3D "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6= 784c0c"},
>> -]
>>=C2=A0 platformdirs =3D [
>>=C2=A0 =C2=A0 =C2=A0 {file =3D "platformdirs-2.5.2-py3-none-an= y.whl", hash =3D "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7e= a5d079477e722bb41ab25788"},
>>=C2=A0 =C2=A0 =C2=A0 {file =3D "platformdirs-2.5.2.tar.gz"= ;, hash =3D "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b= 27b2a3c7feef19"},
>>=C2=A0 ]
>> -ptyprocess =3D []
>>=C2=A0 pycodestyle =3D []
>> +pycparser =3D []
>>=C2=A0 pydocstyle =3D []
>>=C2=A0 pyflakes =3D []
>>=C2=A0 pylama =3D []
>> +pynacl =3D []
>>=C2=A0 pyrsistent =3D []
>>=C2=A0 pyyaml =3D []
>> +six =3D []
>>=C2=A0 snowballstemmer =3D []
>>=C2=A0 toml =3D []
>>=C2=A0 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>&= quot;, "dts@dpdk.org= "]
>>
>>=C2=A0 [tool.poetry.dependencies]
>>=C2=A0 python =3D "^3.10"
>> -pexpect =3D "^4.8.0"
>>=C2=A0 warlock =3D "^2.0.1"
>>=C2=A0 PyYAML =3D "^6.0"
>>=C2=A0 types-PyYAML =3D "^6.0.8"
>> +fabric =3D "^2.7.1"
>>
>>=C2=A0 [tool.poetry.dev-dependencies]
>>=C2=A0 mypy =3D "^0.961"
>> --
>> 2.30.2
>>

Acked-by: Jeremy Spewock <<= a href=3D"mailto:jspewock@iol.unh.edu">jspewock@iol.unh.edu>=C2=A0
--00000000000096d2f505facdbef9--