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