From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id D5D5C42A2E; Tue, 2 May 2023 15:00:16 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 6693440ED8; Tue, 2 May 2023 15:00:16 +0200 (CEST) Received: from mail-ed1-f46.google.com (mail-ed1-f46.google.com [209.85.208.46]) by mails.dpdk.org (Postfix) with ESMTP id 0D1FB40E2D for ; Tue, 2 May 2023 15:00:14 +0200 (CEST) Received: by mail-ed1-f46.google.com with SMTP id 4fb4d7f45d1cf-50bc4b88998so4535785a12.3 for ; Tue, 02 May 2023 06:00:14 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon-tech.20221208.gappssmtp.com; s=20221208; t=1683032414; x=1685624414; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=CpEpZbd1CsyHLF4PqMvRmaCiM8BsuU0ke9CYEx33bRc=; b=Oi2OAGH1HxFvHWJcQoSmdmcIoF4HIrtPWbHFL6c+h6D4U5YbigysbOYYGco8TTTaa9 Ra5JfLs0L1xqcwsnnGGC61XrYr9jOZ04WW0T9CQ/GU3ggwWxquPdhl1YFn5irCfhSQew Abl6L5fuXVwEcSKqE/dEWlfidqN83XPoGD0nXowtYLhgkUpMNjGmZOMlCGDjWZqx5SQ0 93CLNL6nHZqgfsh3v0g5EZl/osGWXx434F5kaTw4dsIf5jxfHioNhSK6dj/eD+q31UxV EOvVlnL2IK5akylOrxIbZhrP2J3VqCluOlw7OETnleJS0/cdk3kySRLaQO1IP36+14Av lv6w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1683032414; x=1685624414; h=content-transfer-encoding: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=CpEpZbd1CsyHLF4PqMvRmaCiM8BsuU0ke9CYEx33bRc=; b=Z1dZ7i/RKMNGjhAIV8NytTq4Fhbq27Cjjx6+W/LYJpUw9gdjD+6Y+t3chwbpjUyaLd KOy6TVQOT7jaG/086Ax0Man2gH/I8ag3VnsAni1qEPwA39mCLVdcuW44+zKQPR4aJrw0 JOoS9+0UrkNMLEg4VBZMyhAzF3l7qgi9Irll57KVxKM10cs7iITQ5PSI0ErE8cDuf9Ia E1/hw6UnZDIgodUFFt2diB7g/qVuF8IeTpsY6Y4t/9Ll3uhK/hBXFwO+oxYEY/C7oOTO 3wE3RTcj1llSRWb1biB4TZb32HqA4U/3Y4ag4cX3IG3xZFjGHlwshs/WtpWoqlcOkQFv wXZA== X-Gm-Message-State: AC+VfDz9l6WcZmtMlEpuw1tJvi+C0PIP5gKGSDXTNGavUCuFm4zDyKQv U/tW/3lGpKBsXji08hees+Ncae8FKksYhcdFGGA53A== X-Google-Smtp-Source: ACHHUZ7gP7aq44vtf1G5QgovWvHJuuuMO7q25r5aSXgGrcImURV7z25iFlLxnSmHCH8rd6stPTX6ZJrZAStrcB4d7x0= X-Received: by 2002:a05:6402:150d:b0:50b:5dbe:e0f6 with SMTP id f13-20020a056402150d00b0050b5dbee0f6mr6592303edw.25.1683032414132; Tue, 02 May 2023 06:00:14 -0700 (PDT) MIME-Version: 1.0 References: <20230403114608.1423020-2-juraj.linkes@pantheon.tech> <20230424133537.58698-1-juraj.linkes@pantheon.tech> In-Reply-To: From: =?UTF-8?Q?Juraj_Linke=C5=A1?= Date: Tue, 2 May 2023 15:00:02 +0200 Message-ID: Subject: Re: [PATCH v2] dts: replace pexpect with fabric To: Jeremy Spewock 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: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org On Fri, Apr 28, 2023 at 9:04=E2=80=AFPM Jeremy Spewock wrote: > > > > On Mon, Apr 24, 2023 at 9:35=E2=80=AFAM Juraj Linke=C5=A1 wrote: >> >> Pexpect is not a dedicated SSH connection library while Fabric is. With >> Fabric, all SSH-related logic is provided and we can just focus on >> what's DTS specific. >> >> Signed-off-by: Juraj Linke=C5=A1 >> --- >> 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 environment = and the other hosts. >> - The pexpect implementation is a wrapper around the ssh command in th= e DTS environment. >> - This means it'll use the SSH agent providing the ssh command and its= keys. >> + DTS uses the Fabric Python library for SSH connections between DTS e= nvironment >> + 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 ``~/.s= sh/`` >> + (with any matching OpenSSH-style certificates). >> + DTS doesn't pass any keys, so Fabric tries to use the other two meth= ods. >> >> >> Setting up System Under Test >> @@ -132,6 +137,21 @@ There are two areas that need to be set up on a Sys= tem Under Test: >> It's possible to use the hugepage configuration already present on= the SUT. >> If you wish to do so, don't specify the hugepage configuration in = the DTS config file. >> >> +#. **User with administrator privileges** >> + >> +.. _sut_admin_user: >> + >> + DTS needs administrator privileges to run DPDK applications (such as= testpmd) on the SUT. >> + The SUT user must be able run commands in privileged mode without as= king for password. >> + On most Linux distributions, it's a matter of setting up passwordles= s sudo: >> + >> + #. Run ``sudo visudo`` and check that it contains ``%sudo ALL=3D(A= LL:ALL) ALL``. >> + >> + #. Add the SUT user to the sudo group with: >> + >> + .. code-block:: console >> + >> + sudo usermod -aG sudo >> >> Running DTS >> ----------- >> @@ -151,7 +171,8 @@ which is a template that illustrates what can be con= figured in DTS: >> :start-at: executions: >> >> >> -The user must be root or any other user with prompt starting with ``#``= . >> +The user must have :ref:`administrator privileges ` >> +which don't require password authentication. >> The other fields are mostly self-explanatory >> and documented in more detail in ``dts/framework/config/conf_yaml_schem= a.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: {', '.jo= in(self.errors)}" >> + >> + return message >> >> >> class SSHSessionDeadError(DTSError): >> diff --git a/dts/framework/remote_session/linux_session.py b/dts/framewo= rk/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 ses= sions. >> """ >> >> + 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, forc= e_first_numa: bool) -> None: >> self._mount_huge_pages() >> >> def _get_hugepage_size(self) -> int: >> - hugepage_size =3D self.remote_session.send_command( >> + hugepage_size =3D self.send_command( >> "awk '/Hugepagesize/ {print $2}' /proc/meminfo" >> ).stdout >> return int(hugepage_size) >> >> def _get_hugepages_total(self) -> int: >> - hugepages_total =3D self.remote_session.send_command( >> + hugepages_total =3D self.send_command( >> "awk '/HugePages_Total/ { print $2 }' /proc/meminfo" >> ).stdout >> return int(hugepages_total) >> >> def _get_numa_nodes(self) -> list[int]: >> try: >> - numa_count =3D self.remote_session.send_command( >> + numa_count =3D self.send_command( >> "cat /sys/devices/system/node/online", verify=3DTrue >> ).stdout >> numa_range =3D expand_range(numa_count) >> @@ -70,14 +71,12 @@ def _get_numa_nodes(self) -> list[int]: >> def _mount_huge_pages(self) -> None: >> self._logger.info("Re-mounting Hugepages.") >> hugapge_fs_cmd =3D "awk '/hugetlbfs/ { print $2 }' /proc/mounts= " >> - self.remote_session.send_command(f"umount $({hugapge_fs_cmd})") >> - result =3D self.remote_session.send_command(hugapge_fs_cmd) >> + self.send_command(f"umount $({hugapge_fs_cmd})") >> + result =3D self.send_command(hugapge_fs_cmd) >> if result.stdout =3D=3D "": >> remote_mount_path =3D "/mnt/huge" >> - self.remote_session.send_command(f"mkdir -p {remote_mount_p= ath}") >> - 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}", p= rivileged=3DTrue) >> hugepage_config_path =3D ( >> f"/sys/devices/system/node/node{self._numa_nodes[0]}/hu= gepages" >> f"/hugepages-{size}kB/nr_hugepages" >> ) >> >> - self.remote_session.send_command( >> - f"echo {amount} | sudo tee {hugepage_config_path}" >> + self.send_command( >> + f"echo {amount} | tee {hugepage_config_path}", privileged= =3DTrue >> ) >> diff --git a/dts/framework/remote_session/os_session.py b/dts/framework/= remote_session/os_session.py >> index 4c48ae2567..bfd70bd480 100644 >> --- a/dts/framework/remote_session/os_session.py >> +++ b/dts/framework/remote_session/os_session.py >> @@ -10,7 +10,7 @@ >> from framework.logger import DTSLOG >> from framework.settings import SETTINGS >> from framework.testbed_model import LogicalCore >> -from framework.utils import EnvVarsDict, MesonArgs >> +from framework.utils import MesonArgs >> >> from .remote import CommandResult, RemoteSession, create_remote_session >> >> @@ -53,17 +53,32 @@ def is_alive(self) -> bool: >> def send_command( >> self, >> command: str, >> - timeout: float, >> + timeout: float =3D SETTINGS.timeout, >> + privileged: bool =3D False, >> verify: bool =3D False, >> - env: EnvVarsDict | None =3D None, >> + env: dict | None =3D None, >> ) -> CommandResult: >> """ >> An all-purpose API in case the command to be executed is alread= y >> OS-agnostic, such as when the path to the executed command has = been >> constructed beforehand. >> """ >> + if privileged: >> + command =3D self._get_privileged_command(command) >> + >> return self.remote_session.send_command(command, timeout, verif= y, env) >> >> + @abstractmethod >> + def _get_privileged_command(self, command: str) -> str: >> + """Modify the command so that it executes with administrative p= rivileges. >> + >> + Args: >> + command: The command to modify. >> + >> + Returns: >> + The modified command that executes with administrative priv= ileges. >> + """ >> + >> @abstractmethod >> def guess_dpdk_remote_dir(self, remote_dir) -> PurePath: >> """ >> @@ -90,17 +105,35 @@ def join_remote_path(self, *args: str | PurePath) -= > PurePath: >> """ >> >> @abstractmethod >> - def copy_file( >> + def copy_from( >> self, >> source_file: str | PurePath, >> destination_file: str | PurePath, >> - source_remote: bool =3D False, >> ) -> None: >> + """Copy a file from the remote Node to the local filesystem. >> + >> + Copy source_file from the remote Node associated with this remo= te >> + session to destination_file on the local filesystem. >> + >> + Args: >> + source_file: the file on the remote Node. >> + destination_file: a file or directory path on the local fil= esystem. >> """ >> + >> + @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_f= ile 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 No= de. >> """ >> >> @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/framewo= rk/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, RemoteCommandExecutionE= rror >> from framework.settings import SETTINGS >> -from framework.utils import EnvVarsDict, MesonArgs >> +from framework.utils import MesonArgs >> >> from .os_session import OSSession >> >> @@ -34,7 +34,7 @@ def combine_short_options(**opts: bool) -> str: >> >> def guess_dpdk_remote_dir(self, remote_dir) -> PurePosixPath: >> remote_guess =3D self.join_remote_path(remote_dir, "dpdk-*") >> - result =3D self.remote_session.send_command(f"ls -d {remote_gue= ss} | tail -1") >> + result =3D self.send_command(f"ls -d {remote_guess} | tail -1") >> return PurePosixPath(result.stdout) >> >> def get_remote_tmp_dir(self) -> PurePosixPath: >> @@ -48,7 +48,7 @@ def get_dpdk_build_env_vars(self, arch: Architecture) = -> dict: >> env_vars =3D {} >> if arch =3D=3D Architecture.i686: >> # find the pkg-config path and store it in PKG_CONFIG_LIBDI= R >> - out =3D self.remote_session.send_command("find /usr -type d= -name pkgconfig") >> + out =3D self.send_command("find /usr -type d -name pkgconfi= g") >> pkg_path =3D "" >> res_path =3D out.stdout.split("\r\n") >> for cur_path in res_path: >> @@ -65,13 +65,19 @@ def get_dpdk_build_env_vars(self, arch: Architecture= ) -> dict: >> def join_remote_path(self, *args: str | PurePath) -> PurePosixPath: >> return PurePosixPath(*args) >> >> - def copy_file( >> + def copy_from( >> self, >> source_file: str | PurePath, >> destination_file: str | PurePath, >> - source_remote: bool =3D False, >> ) -> None: >> - self.remote_session.copy_file(source_file, destination_file, so= urce_remote) >> + self.remote_session.copy_from(source_file, destination_file) >> + >> + def copy_to( >> + self, >> + source_file: str | PurePath, >> + destination_file: str | PurePath, >> + ) -> None: >> + self.remote_session.copy_to(source_file, destination_file) >> >> def remove_remote_dir( >> self, >> @@ -80,24 +86,24 @@ def remove_remote_dir( >> force: bool =3D True, >> ) -> None: >> opts =3D PosixSession.combine_short_options(r=3Drecursive, f=3D= force) >> - self.remote_session.send_command(f"rm{opts} {remote_dir_path}") >> + self.send_command(f"rm{opts} {remote_dir_path}") >> >> def extract_remote_tarball( >> self, >> remote_tarball_path: str | PurePath, >> expected_dir: str | PurePath | None =3D None, >> ) -> None: >> - self.remote_session.send_command( >> + self.send_command( >> f"tar xfm {remote_tarball_path} " >> f"-C {PurePosixPath(remote_tarball_path).parent}", >> 60, >> ) >> if expected_dir: >> - self.remote_session.send_command(f"ls {expected_dir}", veri= fy=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_d= ir}", >> timeout, >> verify=3DTrue, >> @@ -118,7 +124,7 @@ def build_dpdk( >> # fresh build - remove target dir first, then build fro= m scratch >> self._logger.info("Configuring DPDK build from scratch.= ") >> self.remove_remote_dir(remote_dpdk_build_dir) >> - self.remote_session.send_command( >> + self.send_command( >> f"meson setup " >> f"{meson_args} {remote_dpdk_dir} {remote_dpdk_build= _dir}", >> timeout, >> @@ -127,14 +133,14 @@ def build_dpdk( >> ) >> >> self._logger.info("Building DPDK.") >> - self.remote_session.send_command( >> + self.send_command( >> f"ninja -C {remote_dpdk_build_dir}", timeout, verify=3D= True, env=3Denv_vars >> ) >> except RemoteCommandExecutionError as e: >> raise DPDKBuildError(f"DPDK build failed when doing '{e.com= mand}'.") >> >> 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')}", verif= y=3DTrue >> ) >> return out.stdout >> @@ -146,7 +152,7 @@ def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: I= terable[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 | PureP= ath) -> list[str] | None: >> Return a list of directories of the remote_dir. >> If remote_path doesn't exist, return None. >> """ >> - out =3D self.remote_session.send_command( >> + out =3D self.send_command( >> f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'" >> ).stdout >> if "No such file or directory" in out: >> @@ -182,9 +188,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable= [str | PurePath]) -> list[in >> for dpdk_runtime_dir in dpdk_runtime_dirs: >> dpdk_config_file =3D PurePosixPath(dpdk_runtime_dir, "confi= g") >> if self._remote_files_exists(dpdk_config_file): >> - out =3D self.remote_session.send_command( >> - f"lsof -Fp {dpdk_config_file}" >> - ).stdout >> + out =3D self.send_command(f"lsof -Fp {dpdk_config_file}= ").stdout >> if out and "No such file or directory" not in out: >> for out_line in out.splitlines(): >> match =3D re.match(pid_regex, out_line) >> @@ -193,7 +197,7 @@ def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable= [str | PurePath]) -> list[in >> return pids >> >> def _remote_files_exists(self, remote_path: PurePath) -> bool: >> - result =3D self.remote_session.send_command(f"test -e {remote_p= ath}") >> + 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 f= ree 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 option= al env vars >> @@ -141,15 +140,33 @@ def is_alive(self) -> bool: >> """ >> >> @abstractmethod >> - def copy_file( >> + def copy_from( >> self, >> source_file: str | PurePath, >> destination_file: str | PurePath, >> - source_remote: bool =3D False, >> ) -> None: >> + """Copy a file from the remote Node to the local filesystem. >> + >> + Copy source_file from the remote Node associated with this remo= te >> + session to destination_file on the local filesystem. >> + >> + Args: >> + source_file: the file on the remote Node. >> + destination_file: a file or directory path on the local fil= esystem. >> """ >> - Copy source_file from local filesystem to destination_file on t= he remote Node >> - associated with the remote session. >> - If source_remote is True, reverse the direction - copy source_f= ile 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 No= de. >> """ >> diff --git a/dts/framework/remote_session/remote/ssh_session.py b/dts/fr= amework/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 PA= NTHEON.tech to the existing list. Is there a reason you remove the others h= ere 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. >> >> -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, log= ger) >> >> def _connect(self) -> None: >> - """ >> - Create connection to assigned node. >> - """ >> + errors =3D [] >> retry_attempts =3D 10 >> login_timeout =3D 20 if self.port else 10 >> - password_regex =3D ( >> - r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password fo= r .+:)" >> - ) >> - try: >> - for retry_attempt in range(retry_attempts): >> - self.session =3D pxssh.pxssh(encoding=3D"utf-8") >> - try: >> - self.session.login( >> - self.ip, >> - self.username, >> - self.password, >> - original_prompt=3D"[$#>]", >> - port=3Dself.port, >> - login_timeout=3Dlogin_timeout, >> - password_regex=3Dpassword_regex, >> - ) >> - break >> - except Exception as e: >> - self._logger.warning(e) >> - time.sleep(2) >> - self._logger.info( >> - f"Retrying connection: retry number {retry_atte= mpt + 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.host= name} is " >> - f"stopped.\n" >> + for retry_attempt in range(retry_attempts): >> + try: >> + self.session =3D Connection( >> + self.ip, >> + user=3Dself.username, >> + port=3Dself.port, >> + connect_kwargs=3D{"password": self.password}, >> + connect_timeout=3Dlogin_timeout, >> ) >> - self._logger.info(GREEN(suggestion)) >> - >> - raise SSHConnectionError(self.hostname) >> + self.session.open() >> >> - def send_expect( >> - self, command: str, prompt: str, timeout: float =3D 15, verify:= bool =3D False >> - ) -> str | int: >> - try: >> - ret =3D self.send_expect_base(command, prompt, timeout) >> - if verify: >> - ret_status =3D self.send_expect_base("echo $?", prompt,= timeout) >> - try: >> - retval =3D int(ret_status) >> - if retval: >> - self._logger.error(f"Command: {command} failure= !") >> - self._logger.error(ret) >> - return retval >> - else: >> - return ret >> - except ValueError: >> - return ret >> - else: >> - return ret >> - except Exception as e: >> - self._logger.error( >> - f"Exception happened in [{command}] and output is " >> - f"[{self._get_output()}]" >> - ) >> - raise e >> - >> - def send_expect_base(self, command: str, prompt: str, timeout: floa= t) -> 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, AuthenticationExce= ption) as e: >> + self._logger.exception(e) >> + raise SSHConnectionError(self.hostname) from e >> >> - def _prompt(self, command: str, timeout: float) -> None: >> - if not self.session.prompt(timeout): >> - raise SSHTimeoutError(command, self._get_output()) from Non= e >> + except (NoValidConnectionsError, socket.error, SSHException= ) as e: >> + self._logger.debug(traceback.format_exc()) >> + self._logger.warning(e) >> >> - def get_output(self, timeout: float =3D 15) -> str: >> - """ >> - Get all output before timeout >> - """ >> - try: >> - self.session.prompt(timeout) >> - except Exception: >> - pass >> - >> - before =3D self._get_output() >> - self._flush() >> - >> - return before >> + error =3D repr(e) >> + if error not in errors: >> + errors.append(error) >> >> - def _get_output(self) -> str: >> - if not self.is_alive(): >> - raise SSHSessionDeadError(self.hostname) >> - before =3D self.session.before.rsplit("\r\n", 1)[0] >> - if before =3D=3D "[PEXPECT]": >> - return "" >> - return before >> + self._logger.info( >> + f"Retrying connection: retry number {retry_attempt = + 1}." >> + ) >> >> - def _flush(self) -> None: >> - """ >> - Clear all session buffer >> - """ >> - self.session.buffer =3D "" >> - self.session.before =3D "" >> + else: >> + break >> + else: >> + raise SSHConnectionError(self.hostname, errors) >> >> def is_alive(self) -> bool: >> - return self.session.isalive() >> + return self.session.is_connected >> >> def _send_command( >> - self, command: str, timeout: float, env: EnvVarsDict | None >> + self, command: str, timeout: float, env: dict | None >> ) -> CommandResult: >> - output =3D self._send_command_get_output(command, timeout, env) >> - return_code =3D int(self._send_command_get_output("echo $?", ti= meout, None)) >> + """Send a command and return the result of the execution. >> >> - # we're capturing only stdout >> - return CommandResult(self.name, command, output, "", return_cod= e) >> + Args: >> + command: The command to execute. >> + timeout: Wait at most this many seconds for the execution t= o complete. >> + env: Extra environment variables that will be used in comma= nd execution. >> >> - def _send_command_get_output( >> - self, command: str, timeout: float, env: EnvVarsDict | None >> - ) -> str: >> + Raises: >> + SSHSessionDeadError: The session died while executing the c= ommand. >> + 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.re= turn_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}:{destinati= on_file}" >> + self.session.get(str(destination_file), str(source_file)) >> >> - port =3D "" >> - if self.port: >> - port =3D f" -P {self.port}" >> - >> - command =3D ( >> - f"scp -v{port} -o NoHostAuthenticationForLocalhost=3Dyes" >> - f" {source_file} {destination_file}" >> - ) >> - >> - self._spawn_scp(command) >> + def copy_to( >> + self, >> + source_file: str | PurePath, >> + destination_file: str | PurePath, >> + ) -> None: >> + self.session.put(str(source_file), str(destination_file)) >> >> - def _spawn_scp(self, scp_cmd: str) -> None: >> - """ >> - Transfer a file with SCP >> - """ >> - self._logger.info(scp_cmd) >> - p: pexpect.spawn =3D pexpect.spawn(scp_cmd) >> - time.sleep(0.5) >> - ssh_newkey: str =3D "Are you sure you want to continue connecti= ng" >> - i: int =3D p.expect( >> - [ssh_newkey, "[pP]assword", "# ", pexpect.EOF, pexpect.TIME= OUT], 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/tes= tbed_model/sut_node.py >> index 2b2b50d982..9dbc390848 100644 >> --- a/dts/framework/testbed_model/sut_node.py >> +++ b/dts/framework/testbed_model/sut_node.py >> @@ -10,7 +10,7 @@ >> from framework.config import BuildTargetConfiguration, NodeConfiguratio= n >> from framework.remote_session import CommandResult, OSSession >> from framework.settings import SETTINGS >> -from framework.utils import EnvVarsDict, MesonArgs >> +from framework.utils import MesonArgs >> >> from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice >> from .node import Node >> @@ -27,7 +27,7 @@ class SutNode(Node): >> _dpdk_prefix_list: list[str] >> _dpdk_timestamp: str >> _build_target_config: BuildTargetConfiguration | None >> - _env_vars: EnvVarsDict >> + _env_vars: dict >> _remote_tmp_dir: PurePath >> __remote_dpdk_dir: PurePath | None >> _dpdk_version: str | None >> @@ -38,7 +38,7 @@ def __init__(self, node_config: NodeConfiguration): >> super(SutNode, self).__init__(node_config) >> self._dpdk_prefix_list =3D [] >> self._build_target_config =3D None >> - self._env_vars =3D EnvVarsDict() >> + self._env_vars =3D {} >> self._remote_tmp_dir =3D self.main_session.get_remote_tmp_dir() >> self.__remote_dpdk_dir =3D None >> self._dpdk_version =3D None >> @@ -94,7 +94,7 @@ def _configure_build_target( >> """ >> Populate common environment variables and set build target conf= ig. >> """ >> - 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_conf= ig.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._r= emote_tmp_dir) >> + self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._rem= ote_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, verif= y=3DTrue >> ) >> >> >> diff --git a/dts/framework/utils.py b/dts/framework/utils.py >> index 55e0b0ef0e..8cfbc6a29d 100644 >> --- a/dts/framework/utils.py >> +++ b/dts/framework/utils.py >> @@ -42,19 +42,10 @@ def expand_range(range_str: str) -> list[int]: >> return expanded_range >> >> >> -def GREEN(text: str) -> str: >> - return f"\u001B[32;1m{str(text)}\u001B[0m" >> - >> - >> def RED(text: str) -> str: >> return f"\u001B[31;1m{str(text)}\u001B[0m" >> >> >> -class EnvVarsDict(dict): >> - def __str__(self) -> str: >> - return " ".join(["=3D".join(item) for item in self.items()]) >> - >> - >> class MesonArgs(object): >> """ >> Aggregate the arguments needed to build DPDK: >> diff --git a/dts/poetry.lock b/dts/poetry.lock >> index 0b2a007d4d..2438f337cd 100644 >> --- a/dts/poetry.lock >> +++ b/dts/poetry.lock >> @@ -12,6 +12,18 @@ docs =3D ["furo", "sphinx", "zope.interface", "sphinx= -notfound-page"] >> tests =3D ["coverage[toml] (>=3D5.0.2)", "hypothesis", "pympler", "pyte= st (>=3D4.3.0)", "mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins", "zope= .interface", "cloudpickle"] >> tests_no_zope =3D ["coverage[toml] (>=3D5.0.2)", "hypothesis", "pympler= ", "pytest (>=3D4.3.0)", "mypy (>=3D0.900,!=3D0.940)", "pytest-mypy-plugins= ", "cloudpickle"] >> >> +[[package]] >> +name =3D "bcrypt" >> +version =3D "4.0.1" >> +description =3D "Modern password hashing for your software and your ser= vers" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D ">=3D3.6" >> + >> +[package.extras] >> +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)"] >> +typecheck =3D ["mypy"] >> + >> [[package]] >> name =3D "black" >> version =3D "22.10.0" >> @@ -33,6 +45,17 @@ d =3D ["aiohttp (>=3D3.7.4)"] >> jupyter =3D ["ipython (>=3D7.8.0)", "tokenize-rt (>=3D3.2.0)"] >> uvloop =3D ["uvloop (>=3D0.15.2)"] >> >> +[[package]] >> +name =3D "cffi" >> +version =3D "1.15.1" >> +description =3D "Foreign Function Interface for Python calling C code." >> +category =3D "main" >> +optional =3D false >> +python-versions =3D "*" >> + >> +[package.dependencies] >> +pycparser =3D "*" >> + >> [[package]] >> name =3D "click" >> version =3D "8.1.3" >> @@ -52,6 +75,52 @@ category =3D "dev" >> optional =3D false >> python-versions =3D "!=3D3.0.*,!=3D3.1.*,!=3D3.2.*,!=3D3.3.*,!=3D3.4.*,= !=3D3.5.*,!=3D3.6.*,>=3D2.7" >> >> +[[package]] >> +name =3D "cryptography" >> +version =3D "40.0.2" >> +description =3D "cryptography is a package which provides cryptographic= recipes and primitives to Python developers." >> +category =3D "main" >> +optional =3D false >> +python-versions =3D ">=3D3.6" >> + >> +[package.dependencies] >> +cffi =3D ">=3D1.12" >> + >> +[package.extras] >> +docs =3D ["sphinx (>=3D5.3.0)", "sphinx-rtd-theme (>=3D1.1.1)"] >> +docstest =3D ["pyenchant (>=3D1.6.11)", "twine (>=3D1.12.0)", "sphinxco= ntrib-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-ben= chmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso86= 01"] >> +test-randomorder =3D ["pytest-randomly"] >> +tox =3D ["tox"] >> + >> +[[package]] >> +name =3D "fabric" >> +version =3D "2.7.1" >> +description =3D "High level SSH command execution" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D "*" >> + >> +[package.dependencies] >> +invoke =3D ">=3D1.3,<2.0" >> +paramiko =3D ">=3D2.4" >> +pathlib2 =3D "*" >> + >> +[package.extras] >> +pytest =3D ["mock (>=3D2.0.0,<3.0)", "pytest (>=3D3.2.5,<4.0)"] >> +testing =3D ["mock (>=3D2.0.0,<3.0)"] >> + >> +[[package]] >> +name =3D "invoke" >> +version =3D "1.7.3" >> +description =3D "Pythonic task execution" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D "*" >> + >> [[package]] >> name =3D "isort" >> version =3D "5.10.1" >> @@ -136,23 +205,41 @@ optional =3D false >> python-versions =3D "*" >> >> [[package]] >> -name =3D "pathspec" >> -version =3D "0.10.1" >> -description =3D "Utility library for gitignore style pattern matching o= f file paths." >> -category =3D "dev" >> +name =3D "paramiko" >> +version =3D "3.1.0" >> +description =3D "SSH2 protocol library" >> +category =3D "main" >> optional =3D false >> -python-versions =3D ">=3D3.7" >> +python-versions =3D ">=3D3.6" >> + >> +[package.dependencies] >> +bcrypt =3D ">=3D3.2" >> +cryptography =3D ">=3D3.3" >> +pynacl =3D ">=3D1.5" >> + >> +[package.extras] >> +all =3D ["pyasn1 (>=3D0.1.7)", "invoke (>=3D2.0)", "gssapi (>=3D1.4.1)"= , "pywin32 (>=3D2.1.8)"] >> +gssapi =3D ["pyasn1 (>=3D0.1.7)", "gssapi (>=3D1.4.1)", "pywin32 (>=3D2= .1.8)"] >> +invoke =3D ["invoke (>=3D2.0)"] >> >> [[package]] >> -name =3D "pexpect" >> -version =3D "4.8.0" >> -description =3D "Pexpect allows easy control of interactive console app= lications." >> +name =3D "pathlib2" >> +version =3D "2.3.7.post1" >> +description =3D "Object-oriented filesystem paths" >> category =3D "main" >> optional =3D false >> python-versions =3D "*" >> >> [package.dependencies] >> -ptyprocess =3D ">=3D0.5" >> +six =3D "*" >> + >> +[[package]] >> +name =3D "pathspec" >> +version =3D "0.10.1" >> +description =3D "Utility library for gitignore style pattern matching o= f file paths." >> +category =3D "dev" >> +optional =3D false >> +python-versions =3D ">=3D3.7" >> >> [[package]] >> name =3D "platformdirs" >> @@ -166,14 +253,6 @@ python-versions =3D ">=3D3.7" >> docs =3D ["furo (>=3D2021.7.5b38)", "proselint (>=3D0.10.2)", "sphinx-a= utodoc-typehints (>=3D1.12)", "sphinx (>=3D4)"] >> test =3D ["appdirs (=3D=3D1.4.4)", "pytest-cov (>=3D2.7)", "pytest-mock= (>=3D3.6)", "pytest (>=3D6)"] >> >> -[[package]] >> -name =3D "ptyprocess" >> -version =3D "0.7.0" >> -description =3D "Run a subprocess in a pseudo terminal" >> -category =3D "main" >> -optional =3D false >> -python-versions =3D "*" >> - >> [[package]] >> name =3D "pycodestyle" >> version =3D "2.9.1" >> @@ -182,6 +261,14 @@ category =3D "dev" >> optional =3D false >> python-versions =3D ">=3D3.6" >> >> +[[package]] >> +name =3D "pycparser" >> +version =3D "2.21" >> +description =3D "C parser in Python" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*, !=3D3.3.= *" >> + >> [[package]] >> name =3D "pydocstyle" >> version =3D "6.1.1" >> @@ -228,6 +315,21 @@ tests =3D ["pytest (>=3D7.1.2)", "pytest-mypy", "er= adicate (>=3D2.0.0)", "radon (>=3D5.1 >> toml =3D ["toml (>=3D0.10.2)"] >> vulture =3D ["vulture"] >> >> +[[package]] >> +name =3D "pynacl" >> +version =3D "1.5.0" >> +description =3D "Python binding to the Networking and Cryptography (NaC= l) library" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D ">=3D3.6" >> + >> +[package.dependencies] >> +cffi =3D ">=3D1.4.1" >> + >> +[package.extras] >> +docs =3D ["sphinx (>=3D1.6.5)", "sphinx-rtd-theme"] >> +tests =3D ["pytest (>=3D3.2.1,!=3D3.3.0)", "hypothesis (>=3D3.27.0)"] >> + >> [[package]] >> name =3D "pyrsistent" >> version =3D "0.19.1" >> @@ -244,6 +346,14 @@ category =3D "main" >> optional =3D false >> python-versions =3D ">=3D3.6" >> >> +[[package]] >> +name =3D "six" >> +version =3D "1.16.0" >> +description =3D "Python 2 and 3 compatibility utilities" >> +category =3D "main" >> +optional =3D false >> +python-versions =3D ">=3D2.7, !=3D3.0.*, !=3D3.1.*, !=3D3.2.*" >> + >> [[package]] >> name =3D "snowballstemmer" >> version =3D "2.2.0" >> @@ -299,13 +409,18 @@ jsonschema =3D ">=3D4,<5" >> [metadata] >> lock-version =3D "1.1" >> python-versions =3D "^3.10" >> -content-hash =3D "a0f040b07fc6ce4deb0be078b9a88c2a465cb6bccb9e260a67e92= c2403e2319f" >> +content-hash =3D "719c43bcaa5d181921debda884f8f714063df0b2336d61e9f64ec= ab034e8b139" >> >> [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:0b= 48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, >> - {file =3D "pexpect-4.8.0.tar.gz", hash =3D "sha256:fc65a43959d153d0= 114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, >> -] >> platformdirs =3D [ >> {file =3D "platformdirs-2.5.2-py3-none-any.whl", hash =3D "sha256:0= 27d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, >> {file =3D "platformdirs-2.5.2.tar.gz", hash =3D "sha256:58c8abb07dc= b441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, >> ] >> -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 ", "d= ts@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 >>