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 1173F42EAB; Tue, 18 Jul 2023 17:19:09 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id F19F6410D3; Tue, 18 Jul 2023 17:19:08 +0200 (CEST) Received: from mail-pg1-f170.google.com (mail-pg1-f170.google.com [209.85.215.170]) by mails.dpdk.org (Postfix) with ESMTP id 2022440A84 for ; Tue, 18 Jul 2023 17:19:07 +0200 (CEST) Received: by mail-pg1-f170.google.com with SMTP id 41be03b00d2f7-55be1ce1669so3595798a12.1 for ; Tue, 18 Jul 2023 08:19:07 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=iol.unh.edu; s=unh-iol; t=1689693546; x=1692285546; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=o8q3z6eOSfSN6fciVbJYxlSwhIuNsYFQM8CysnmyaZ4=; b=gcn1H9SSfv43fiM+OPWFm7SE7n9F3V+1E3TVOBoSmKo+ULFbWbTUasVCK9lTgcI/H2 FJn+BZ6ityUmc9xibNr5ygSQxq0qtS2TUEj5oHM4waX3v9imcgjGa6jzDeKFcNU1bt0u QPoQa9UCkNf5mwTgkUdYlc0m4uN/RNxd7f2lk= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1689693546; x=1692285546; 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=o8q3z6eOSfSN6fciVbJYxlSwhIuNsYFQM8CysnmyaZ4=; b=eE6AAG+cIKmyEFhwBMGyoLGSj8kNJa6qNgjW4W1MwOmc0tuidgiC3gTo48wr7RKOY8 7o/JzazFgUdOr4LAC5txYP4clZzTLeYCDBJoUEvFcZXKvR5oAe73sW1udqs8TsOYm1Zg J7xvZnlFb+My/oTtG1rpVQWO307h8YGeickvQ3dP+9B4USBRfqF7xOgsgGUa93LbkeaG JZSE3HTqfROMMTB106lYOkEFu7yTjX6D7rZ/tY0ifsfGDCJd8/81TxIo+sB1JANt0shN HajzroX+0SDSDSdLAnYmBUQ3rYhV1Y6FoMJkGf9RrhN/y2VFrh/lGBLuvQVG/pYjEOir anUA== X-Gm-Message-State: ABy/qLb2YZsyenYXZzh+2xhhUegu+/+tD/lui5vCmnC5euiiqIkQ44Ms yeQRSp6BXuxBBwN3zwLT7S+9bCMtxSy6M/2mw4W0VQ== X-Google-Smtp-Source: APBJJlHTbn9bE/v3OtvUOQf/AbvjKdsr8gJH4WWFEXloZR1Z1eooafTziUAtYDHn/OYEvm3WD8Dg7EDgx2U/DTPbeUc= X-Received: by 2002:a17:90a:31cd:b0:262:e2a9:bcf with SMTP id j13-20020a17090a31cd00b00262e2a90bcfmr10766987pjf.49.1689693546317; Tue, 18 Jul 2023 08:19:06 -0700 (PDT) MIME-Version: 1.0 References: <20230717193705.26594-2-jspewock@iol.unh.edu> <20230717193705.26594-3-jspewock@iol.unh.edu> In-Reply-To: From: Jeremy Spewock Date: Tue, 18 Jul 2023 11:18:55 -0400 Message-ID: Subject: Re: [PATCH v8 1/1] dts: add smoke tests To: =?UTF-8?Q?Juraj_Linke=C5=A1?= Cc: Honnappa.Nagarahalli@arm.com, thomas@monjalon.net, lijuan.tu@intel.com, wathsala.vithanage@arm.com, probb@iol.unh.edu, dev@dpdk.org Content-Type: multipart/alternative; boundary="0000000000009bacf40600c46ff6" 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 --0000000000009bacf40600c46ff6 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Tue, Jul 18, 2023 at 8:08=E2=80=AFAM Juraj Linke=C5=A1 wrote: > A few more things I noticed while running DTS. > > On Mon, Jul 17, 2023 at 9:37=E2=80=AFPM wrote: > > > > From: Jeremy Spewock > > > > Adds a new test suite for running smoke tests that verify general > > configuration aspects of the system under test. If any of these tests > > fail, the DTS execution terminates as part of a "fail-fast" model. > > > > Signed-off-by: Jeremy Spewock > > --- > > dts/conf.yaml | 17 +- > > dts/framework/config/__init__.py | 79 ++++++-- > > dts/framework/config/conf_yaml_schema.json | 142 ++++++++++++++- > > dts/framework/dts.py | 84 ++++++--- > > dts/framework/exception.py | 12 ++ > > dts/framework/remote_session/__init__.py | 13 +- > > dts/framework/remote_session/os_session.py | 48 ++++- > > dts/framework/remote_session/posix_session.py | 29 ++- > > .../remote_session/remote/__init__.py | 10 ++ > > .../remote/interactive_remote_session.py | 82 +++++++++ > > .../remote/interactive_shell.py | 98 ++++++++++ > > .../remote_session/remote/testpmd_shell.py | 46 +++++ > > dts/framework/test_result.py | 24 ++- > > dts/framework/test_suite.py | 10 +- > > dts/framework/testbed_model/node.py | 43 ++++- > > dts/framework/testbed_model/sut_node.py | 169 +++++++++++++----- > > dts/framework/utils.py | 3 + > > dts/tests/TestSuite_smoke_tests.py | 114 ++++++++++++ > > 18 files changed, 931 insertions(+), 92 deletions(-) > > create mode 100644 > dts/framework/remote_session/remote/interactive_remote_session.py > > create mode 100644 > dts/framework/remote_session/remote/interactive_shell.py > > create mode 100644 dts/framework/remote_session/remote/testpmd_shell.p= y > > create mode 100644 dts/tests/TestSuite_smoke_tests.py > > > > diff --git a/dts/conf.yaml b/dts/conf.yaml > > index 129801d87c..3a5d87cb49 100644 > > --- a/dts/conf.yaml > > +++ b/dts/conf.yaml > > @@ -10,9 +10,13 @@ executions: > > compiler_wrapper: ccache > > perf: false > > func: true > > + skip_smoke_tests: false # optional flag that allow you to skip > smoke tests > > Typo: allows > > Good catch, thank you. > > > > test_suites: > > - hello_world > > - system_under_test: "SUT 1" > > + system_under_test: > > + node_name: "SUT 1" > > + vdevs: # optional; if removed, vdevs won't be used in the > execution > > + - "crypto_openssl" > > nodes: > > - name: "SUT 1" > > hostname: sut1.change.me.localhost > > > > > diff --git a/dts/framework/config/conf_yaml_schema.json > b/dts/framework/config/conf_yaml_schema.json > > index ca2d4a1ef2..09fcbaf498 100644 > > --- a/dts/framework/config/conf_yaml_schema.json > > +++ b/dts/framework/config/conf_yaml_schema.json > > @@ -6,6 +6,76 @@ > > "type": "string", > > "description": "A unique identifier for a node" > > }, > > + "NIC": { > > + "type": "string", > > + "enum": [ > > + "ALL", > > + "ConnectX3_MT4103", > > + "ConnectX4_LX_MT4117", > > + "ConnectX4_MT4115", > > + "ConnectX5_MT4119", > > + "ConnectX5_MT4121", > > + "I40E_10G-10G_BASE_T_BC", > > + "I40E_10G-10G_BASE_T_X722", > > + "I40E_10G-SFP_X722", > > + "I40E_10G-SFP_XL710", > > + "I40E_10G-X722_A0", > > + "I40E_1G-1G_BASE_T_X722", > > + "I40E_25G-25G_SFP28", > > + "I40E_40G-QSFP_A", > > + "I40E_40G-QSFP_B", > > + "IAVF-ADAPTIVE_VF", > > + "IAVF-VF", > > + "IAVF_10G-X722_VF", > > + "ICE_100G-E810C_QSFP", > > + "ICE_25G-E810C_SFP", > > + "ICE_25G-E810_XXV_SFP", > > + "IGB-I350_VF", > > + "IGB_1G-82540EM", > > + "IGB_1G-82545EM_COPPER", > > + "IGB_1G-82571EB_COPPER", > > + "IGB_1G-82574L", > > + "IGB_1G-82576", > > + "IGB_1G-82576_QUAD_COPPER", > > + "IGB_1G-82576_QUAD_COPPER_ET2", > > + "IGB_1G-82580_COPPER", > > + "IGB_1G-I210_COPPER", > > + "IGB_1G-I350_COPPER", > > + "IGB_1G-I354_SGMII", > > + "IGB_1G-PCH_LPTLP_I218_LM", > > + "IGB_1G-PCH_LPTLP_I218_V", > > + "IGB_1G-PCH_LPT_I217_LM", > > + "IGB_1G-PCH_LPT_I217_V", > > + "IGB_2.5G-I354_BACKPLANE_2_5GBPS", > > + "IGC-I225_LM", > > + "IGC-I226_LM", > > + "IXGBE_10G-82599_SFP", > > + "IXGBE_10G-82599_SFP_SF_QP", > > + "IXGBE_10G-82599_T3_LOM", > > + "IXGBE_10G-82599_VF", > > + "IXGBE_10G-X540T", > > + "IXGBE_10G-X540_VF", > > + "IXGBE_10G-X550EM_A_SFP", > > + "IXGBE_10G-X550EM_X_10G_T", > > + "IXGBE_10G-X550EM_X_SFP", > > + "IXGBE_10G-X550EM_X_VF", > > + "IXGBE_10G-X550T", > > + "IXGBE_10G-X550_VF", > > + "brcm_57414", > > + "brcm_P2100G", > > + "cavium_0011", > > + "cavium_a034", > > + "cavium_a063", > > + "cavium_a064", > > + "fastlinq_ql41000", > > + "fastlinq_ql41000_vf", > > + "fastlinq_ql45000", > > + "fastlinq_ql45000_vf", > > + "hi1822", > > + "virtio" > > + ] > > + }, > > + > > "ARCH": { > > "type": "string", > > "enum": [ > > @@ -94,6 +164,19 @@ > > "amount" > > ] > > }, > > + "pci_address": { > > + "type": "string", > > + "pattern": > "^[\\da-fA-F]{4}:[\\da-fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$" > > + }, > > + "port_peer_address": { > > + "description": "Peer is a TRex port, and IXIA port or a PCI > address", > > + "oneOf": [ > > + { > > + "description": "PCI peer port", > > + "$ref": "#/definitions/pci_address" > > + } > > + ] > > + }, > > "test_suite": { > > "type": "string", > > "enum": [ > > @@ -165,6 +248,44 @@ > > }, > > "hugepages": { > > "$ref": "#/definitions/hugepages" > > + }, > > + "ports": { > > + "type": "array", > > + "items": { > > + "type": "object", > > + "description": "Each port should be described on both > sides of the connection. This makes configuration slightly more verbose b= ut > greatly simplifies implementation. If there are an inconsistencies, then > DTS will not run until that issue is fixed. An example inconsistency woul= d > be port 1, node 1 says it is connected to port 1, node 2, but port 1, nod= e > 2 says it is connected to port 2, node 1.", > > Typo: extra an in are an inconsistencies > > Fixed, thank you. > > + "properties": { > > + "pci": { > > + "$ref": "#/definitions/pci_address", > > + "description": "The local PCI address of the port" > > + }, > > + "os_driver_for_dpdk": { > > + "type": "string", > > + "description": "The driver that the kernel should > bind this device to for DPDK to use it. (ex: vfio-pci)" > > + }, > > + "os_driver": { > > + "type": "string", > > + "description": "The driver normally used by this por= t > (ex: i40e)" > > + }, > > + "peer_node": { > > + "type": "string", > > + "description": "The name of the node the peer port i= s > on" > > + }, > > + "peer_pci": { > > + "$ref": "#/definitions/pci_address", > > + "description": "The PCI address of the peer port" > > + } > > + }, > > + "additionalProperties": false, > > + "required": [ > > + "pci", > > + "os_driver_for_dpdk", > > + "os_driver", > > + "peer_node", > > + "peer_pci" > > + ] > > + }, > > + "minimum": 1 > > } > > }, > > "additionalProperties": false, > > > > > diff --git a/dts/framework/remote_session/remote/interactive_shell.py > b/dts/framework/remote_session/remote/interactive_shell.py > > new file mode 100644 > > index 0000000000..4d9c7638a5 > > --- /dev/null > > +++ b/dts/framework/remote_session/remote/interactive_shell.py > > @@ -0,0 +1,98 @@ > > +# SPDX-License-Identifier: BSD-3-Clause > > +# Copyright(c) 2023 University of New Hampshire > > + > > +from pathlib import PurePath > > +from typing import Callable > > + > > +from paramiko import Channel, SSHClient, channel # type: ignore > > + > > +from framework.logger import DTSLOG > > +from framework.settings import SETTINGS > > + > > + > > +class InteractiveShell: > > + > > + _interactive_session: SSHClient > > + _stdin: channel.ChannelStdinFile > > + _stdout: channel.ChannelFile > > + _ssh_channel: Channel > > + _logger: DTSLOG > > + _timeout: float > > + _startup_command: str > > + _app_args: str > > + _default_prompt: str =3D "" > > + _privileged: bool > > + _get_privileged_command: Callable[[str], str] > > + # Allows for app specific extra characters to be appended to > commands > > + _command_extra_chars: str =3D "" > > + path: PurePath > > + dpdk_app: bool =3D False > > + > > + def __init__( > > + self, > > + interactive_session: SSHClient, > > + logger: DTSLOG, > > + startup_command: str, > > + privileged: bool, > > + _get_privileged_command: Callable[[str], str], > > + app_args: str =3D "", > > + timeout: float =3D SETTINGS.timeout, > > + ) -> None: > > + self._interactive_session =3D interactive_session > > + self._ssh_channel =3D self._interactive_session.invoke_shell() > > + self._stdin =3D self._ssh_channel.makefile_stdin("w") > > + self._stdout =3D self._ssh_channel.makefile("r") > > + self._ssh_channel.settimeout(timeout) > > + self._ssh_channel.set_combine_stderr(True) # combines stdout > and stderr streams > > + self._logger =3D logger > > + self._timeout =3D timeout > > + self._startup_command =3D startup_command > > + self._app_args =3D app_args > > + self._get_privileged_command =3D _get_privileged_command # ty= pe: > ignore > > + self._privileged =3D privileged > > + self._start_application() > > + > > + def _start_application(self) -> None: > > + """Starts a new interactive application based on > _startup_command. > > + > > + This method is often overridden by subclasses as their process > for > > + starting may look different. > > + """ > > + start_command =3D f"{self._startup_command} {self._app_args}" > > + if self._privileged: > > + start_command =3D > self._get_privileged_command(start_command) # type: ignore > > + self.send_command(start_command) > > + > > + def send_command(self, command: str, prompt: str | None =3D None) = -> > str: > > + """Send a command and get all output before the expected endin= g > string. > > + > > + Lines that expect input are not included in the stdout buffer > so they cannot be > > + used for expect. For example, if you were prompted to log into > something > > + with a username and password, you cannot expect "username:" > because it won't > > + yet be in the stdout buffer. A work around for this could be > consuming an > > + extra newline character to force the current prompt into the > stdout buffer. > > + > > + Returns: > > + All output in the buffer before expected string > > + """ > > + self._logger.info(f"Sending command {command.strip()}...") > > Let's unite this log with with remote remote session: > self._logger.info( > f"Sending: '{command}'" + (f" with env vars: '{env}'" if env else "") > ) > > We don't have env vars, but the rest should be the same. > > That makes sense, we want to be sure that it's uniform. I'll fix this. > > > + if prompt is None: > > + prompt =3D self._default_prompt > > + self._stdin.write(f"{command}{self._command_extra_chars}\n") > > + self._stdin.flush() > > + out: str =3D "" > > + for line in self._stdout: > > + out +=3D line > > + if prompt in line and not line.rstrip().endswith( > > + command.rstrip() > > + ): # ignore line that sent command > > + break > > + self._logger.debug(f"Got output: {out}") > > + return out > > + > > + def close(self) -> None: > > + self._stdin.close() > > + self._ssh_channel.close() > > + > > + def __del__(self) -> None: > > + self.close() > Thank you again for the comments, Jeremy --0000000000009bacf40600c46ff6 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


<= div dir=3D"ltr" class=3D"gmail_attr">On Tue, Jul 18, 2023 at 8:08=E2=80=AFA= M Juraj Linke=C5=A1 <juraj.linkes@pantheon.tech> wrote:
A few more things I noticed w= hile running DTS.

On Mon, Jul 17, 2023 at 9:37=E2=80=AFPM <jspewock@iol.unh.edu> wrote:
>
> From: Jeremy Spewock <jspewock@iol.unh.edu>
>
> Adds a new test suite for running smoke tests that verify general
> configuration aspects of the system under test. If any of these tests<= br> > fail, the DTS execution terminates as part of a "fail-fast" = model.
>
> Signed-off-by: Jeremy Spewock <jspewock@iol.unh.edu>
> ---
>=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 17 +-
>=C2=A0 dts/framework/config/__init__.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 |=C2=A0 79 ++++++--
>=C2=A0 dts/framework/config/conf_yaml_schema.json=C2=A0 =C2=A0 | 142 ++= ++++++++++++-
>=C2=A0 dts/framework/dts.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=A0 84 ++++++---
>=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 12 ++
>=C2=A0 dts/framework/remote_session/__init__.py=C2=A0 =C2=A0 =C2=A0 |= =C2=A0 13 +-
>=C2=A0 dts/framework/remote_session/os_session.py=C2=A0 =C2=A0 |=C2=A0 = 48 ++++-
>=C2=A0 dts/framework/remote_session/posix_session.py |=C2=A0 29 ++-
>=C2=A0 .../remote_session/remote/__init__.py=C2=A0 =C2=A0 =C2=A0 =C2=A0= =C2=A0|=C2=A0 10 ++
>=C2=A0 .../remote/interactive_remote_session.py=C2=A0 =C2=A0 =C2=A0 |= =C2=A0 82 +++++++++
>=C2=A0 .../remote/interactive_shell.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0|=C2=A0 98 ++++++++++
>=C2=A0 .../remote_session/remote/testpmd_shell.py=C2=A0 =C2=A0 |=C2=A0 = 46 +++++
>=C2=A0 dts/framework/test_result.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 |=C2=A0 24 ++-
>=C2=A0 dts/framework/test_suite.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0|=C2=A0 10 +-
>=C2=A0 dts/framework/testbed_model/node.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0|=C2=A0 43 ++++-
>=C2=A0 dts/framework/testbed_model/sut_node.py=C2=A0 =C2=A0 =C2=A0 =C2= =A0| 169 +++++++++++++-----
>=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=A03 +
>=C2=A0 dts/tests/TestSuite_smoke_tests.py=C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0 =C2=A0 | 114 ++++++++++++
>=C2=A0 18 files changed, 931 insertions(+), 92 deletions(-)
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/interacti= ve_remote_session.py
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/interacti= ve_shell.py
>=C2=A0 create mode 100644 dts/framework/remote_session/remote/testpmd_s= hell.py
>=C2=A0 create mode 100644 dts/tests/TestSuite_smoke_tests.py
>
> diff --git a/dts/conf.yaml b/dts/conf.yaml
> index 129801d87c..3a5d87cb49 100644
> --- a/dts/conf.yaml
> +++ b/dts/conf.yaml
> @@ -10,9 +10,13 @@ executions:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 compiler_wrapper: ccache
>=C2=A0 =C2=A0 =C2=A0 perf: false
>=C2=A0 =C2=A0 =C2=A0 func: true
> +=C2=A0 =C2=A0 skip_smoke_tests: false # optional flag that allow you = to skip smoke tests

Typo: allows


Good catch, thank you.
=C2=A0=
>
>=C2=A0 =C2=A0 =C2=A0 test_suites:
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 - hello_world
> -=C2=A0 =C2=A0 system_under_test: "SUT 1"
> +=C2=A0 =C2=A0 system_under_test:
> +=C2=A0 =C2=A0 =C2=A0 node_name: "SUT 1"
> +=C2=A0 =C2=A0 =C2=A0 vdevs: # optional; if removed, vdevs won't b= e used in the execution
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 - "crypto_openssl"
>=C2=A0 nodes:
>=C2=A0 =C2=A0 - name: "SUT 1"
>=C2=A0 =C2=A0 =C2=A0 hostname: sut1.change.me.localhost

<snip>

> diff --git a/dts/framework/config/conf_yaml_schema.json b/dts/framewor= k/config/conf_yaml_schema.json
> index ca2d4a1ef2..09fcbaf498 100644
> --- a/dts/framework/config/conf_yaml_schema.json
> +++ b/dts/framework/config/conf_yaml_schema.json
> @@ -6,6 +6,76 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "A unique ide= ntifier for a node"
>=C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "NIC": {
> +=C2=A0 =C2=A0 =C2=A0 "type": "string",
> +=C2=A0 =C2=A0 =C2=A0 "enum": [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ALL",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX3_MT4103",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX4_LX_MT4117",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX4_MT4115",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX5_MT4119",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ConnectX5_MT4121",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-10G_BASE_T_BC",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-10G_BASE_T_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-SFP_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-SFP_XL710",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_10G-X722_A0",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_1G-1G_BASE_T_X722",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_25G-25G_SFP28",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_40G-QSFP_A",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "I40E_40G-QSFP_B",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF-ADAPTIVE_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF-VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IAVF_10G-X722_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_100G-E810C_QSFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_25G-E810C_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "ICE_25G-E810_XXV_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB-I350_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82540EM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82545EM_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82571EB_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82574L",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576_QUAD_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82576_QUAD_COPPER_ET2",=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-82580_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I210_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I350_COPPER",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-I354_SGMII",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPTLP_I218_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPTLP_I218_V",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPT_I217_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_1G-PCH_LPT_I217_V",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGB_2.5G-I354_BACKPLANE_2_5GBPS&quo= t;,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGC-I225_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IGC-I226_LM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_SFP_SF_QP", > +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_T3_LOM",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-82599_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X540T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X540_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_A_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_10G_T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_SFP",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550EM_X_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550T",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "IXGBE_10G-X550_VF",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "brcm_57414",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "brcm_P2100G",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_0011",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a034",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a063",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "cavium_a064",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql41000",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql41000_vf",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql45000",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "fastlinq_ql45000_vf",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "hi1822",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 "virtio"
> +=C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 },
> +
>=C2=A0 =C2=A0 =C2=A0 "ARCH": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "enum": [
> @@ -94,6 +164,19 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "amount"
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
>=C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "pci_address": {
> +=C2=A0 =C2=A0 =C2=A0 "type": "string",
> +=C2=A0 =C2=A0 =C2=A0 "pattern": "^[\\da-fA-F]{4}:[\\da= -fA-F]{2}:[\\da-fA-F]{2}.\\d:?\\w*$"
> +=C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 "port_peer_address": {
> +=C2=A0 =C2=A0 =C2=A0 "description": "Peer is a TRex po= rt, and IXIA port or a PCI address",
> +=C2=A0 =C2=A0 =C2=A0 "oneOf": [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description": "PCI= peer port",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": "#/definiti= ons/pci_address"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 "test_suite": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "string",
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 "enum": [
> @@ -165,6 +248,44 @@
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "hugepages": {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "$ref": &quo= t;#/definitions/hugepages"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "ports": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": "arr= ay",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "items": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "type": &q= uot;object",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "description&qu= ot;: "Each port should be described on both sides of the connection. T= his makes configuration slightly more verbose but greatly simplifies implem= entation. If there are an inconsistencies, then DTS will not run until that= issue is fixed. An example inconsistency would be port 1, node 1 says it i= s connected to port 1, node 2, but port 1, node 2 says it is connected to p= ort 2, node 1.",

Typo: extra an in are an inconsistencies


Fixed, thank you.

=C2=A0<= /div>
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "properties&quo= t;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "pci&quo= t;: {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= $ref": "#/definitions/pci_address",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The local PCI address of the port"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er_for_dpdk": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The driver that the kernel should bind this device= to for DPDK to use it. (ex: vfio-pci)"
> +=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 "os_driv= er": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The driver normally used by this port (ex: i40e)&q= uot;
> +=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 "peer_no= de": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= type": "string",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The name of the node the peer port is on"
> +=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 "peer_pc= i": {
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= $ref": "#/definitions/pci_address",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "= description": "The PCI address of the peer port"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "additionalProp= erties": false,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "required"= : [
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "pci&quo= t;,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er_for_dpdk",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "os_driv= er",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_no= de",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "peer_pc= i"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "minimum": 1
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 },
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "additionalProperties": fa= lse,

<snip>

> diff --git a/dts/framework/remote_session/remote/interactive_shell.py = b/dts/framework/remote_session/remote/interactive_shell.py
> new file mode 100644
> index 0000000000..4d9c7638a5
> --- /dev/null
> +++ b/dts/framework/remote_session/remote/interactive_shell.py
> @@ -0,0 +1,98 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 University of New Hampshire
> +
> +from pathlib import PurePath
> +from typing import Callable
> +
> +from paramiko import Channel, SSHClient, channel=C2=A0 # type: ignore=
> +
> +from framework.logger import DTSLOG
> +from framework.settings import SETTINGS
> +
> +
> +class InteractiveShell:
> +
> +=C2=A0 =C2=A0 _interactive_session: SSHClient
> +=C2=A0 =C2=A0 _stdin: channel.ChannelStdinFile
> +=C2=A0 =C2=A0 _stdout: channel.ChannelFile
> +=C2=A0 =C2=A0 _ssh_channel: Channel
> +=C2=A0 =C2=A0 _logger: DTSLOG
> +=C2=A0 =C2=A0 _timeout: float
> +=C2=A0 =C2=A0 _startup_command: str
> +=C2=A0 =C2=A0 _app_args: str
> +=C2=A0 =C2=A0 _default_prompt: str =3D ""
> +=C2=A0 =C2=A0 _privileged: bool
> +=C2=A0 =C2=A0 _get_privileged_command: Callable[[str], str]
> +=C2=A0 =C2=A0 # Allows for app specific extra characters to be append= ed to commands
> +=C2=A0 =C2=A0 _command_extra_chars: str =3D ""
> +=C2=A0 =C2=A0 path: PurePath
> +=C2=A0 =C2=A0 dpdk_app: bool =3D False
> +
> +=C2=A0 =C2=A0 def __init__(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 interactive_session: SSHClient,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 logger: DTSLOG,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 startup_command: str,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 privileged: bool,
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 _get_privileged_command: Callable[[str], = str],
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 app_args: str =3D "",
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 timeout: float =3D SETTINGS.timeout,
> +=C2=A0 =C2=A0 ) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._interactive_session =3D interactive= _session
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel =3D self._interactive_s= ession.invoke_shell()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin =3D self._ssh_channel.makefil= e_stdin("w")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdout =3D self._ssh_channel.makefi= le("r")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.settimeout(timeout)
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.set_combine_stderr(True= )=C2=A0 # combines stdout and stderr streams
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger =3D logger
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._timeout =3D timeout
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._startup_command =3D startup_command=
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._app_args =3D app_args
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._get_privileged_command =3D _get_pri= vileged_command=C2=A0 # type: ignore
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._privileged =3D privileged
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._start_application()
> +
> +=C2=A0 =C2=A0 def _start_application(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Starts a new interactiv= e application based on _startup_command.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 This method is often overridden by subcla= sses as their process for
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 starting may look different.
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 start_command =3D f"{self._startup_c= ommand} {self._app_args}"
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._privileged:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 start_command =3D self._get= _privileged_command(start_command)=C2=A0 # type: ignore
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.send_command(start_command)
> +
> +=C2=A0 =C2=A0 def send_command(self, command: str, prompt: str | None= =3D None) -> str:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Send a command and get = all output before the expected ending string.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Lines that expect input are not included = in the stdout buffer so they cannot be
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 used for expect. For example, if you were= prompted to log into something
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 with a username and password, you cannot = expect "username:" because it won't
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 yet be in the stdout buffer. A work aroun= d for this could be consuming an
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 extra newline character to force the curr= ent prompt into the stdout buffer.
> +
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 Returns:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 All output in the buffer be= fore expected string
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 """
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.info(f"Sending command {c= ommand.strip()}...")

Let's unite this log with with remote remote session:
self._l= ogger.info(
f"Sending: '{command}'" + (f" with env vars: '{e= nv}'" if env else "")
)

We don't have env vars, but the rest should be the same.


That makes sense, we want to be sure that it&= #39;s uniform. I'll fix this.
=C2=A0

> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 if prompt is None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 prompt =3D self._default_pr= ompt
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.write(f"{command}{self._= command_extra_chars}\n")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.flush()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 out: str =3D ""
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 for line in self._stdout:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 out +=3D line
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if prompt in line and not l= ine.rstrip().endswith(
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 command.rstri= p()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 ):=C2=A0 # ignore line that= sent command
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 break
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._logger.debug(f"Got output: {ou= t}")
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 return out
> +
> +=C2=A0 =C2=A0 def close(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._stdin.close()
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._ssh_channel.close()
> +
> +=C2=A0 =C2=A0 def __del__(self) -> None:
> +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self.close()

Than= k you again for the comments,
Jeremy
--0000000000009bacf40600c46ff6--