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 B680742EB9; Wed, 19 Jul 2023 16:03:02 +0200 (CEST) Received: from mails.dpdk.org (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id A3F3A40F16; Wed, 19 Jul 2023 16:03:02 +0200 (CEST) Received: from mail-ed1-f43.google.com (mail-ed1-f43.google.com [209.85.208.43]) by mails.dpdk.org (Postfix) with ESMTP id 4ED7B40DFD for ; Wed, 19 Jul 2023 16:03:01 +0200 (CEST) Received: by mail-ed1-f43.google.com with SMTP id 4fb4d7f45d1cf-51e99584a82so9513905a12.1 for ; Wed, 19 Jul 2023 07:03:01 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pantheon.tech; s=google; t=1689775381; x=1690380181; 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=3Fjnpu9MM5y56e6rfuMm/++MTl6WXpzkxPvvaa3tB5c=; b=YOAqBdGH/Q6CeO9Wmx0iWHFY/2kyPla3rA25W/pmqIaJBbLj0xf0A+MzmUnYjbGaao V9Wco2KxAo7SKHMG1vsyiMnD539jkhTbFVNMOIke9ngePa+wWnLLmCRZE54pH8WLOo57 uMdXoSx3MFa5GpDjos5/BGriWH3KGkTwfFuRo27GIpO4UZMoxsiv2TVHjEdqiXa86Ach lqEH7dqbSz6T1Foo9ZHw/edYrCVXl8un7LRzCJiL69O/hnQ+G3tUv/zhYPqAnjGBx35i IZ8ssMQoQ7zDPssOQMVbWdBikF4OzKgWAvRcTCJ2nk54iZf6YyORs6KSkcgCQNKiRvoY kdDA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1689775381; x=1690380181; 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=3Fjnpu9MM5y56e6rfuMm/++MTl6WXpzkxPvvaa3tB5c=; b=Q0XqF25CFXvphq1MONzLw+ImNwOShbuMipWdoM7Vy9tRTMRHCsXdkjNJJQbuHsTEQL ydd4Mr7H1DaO9iJ6htSU/oHbnz91y6YBUgKH+x1LKGYOdmj0yIYhHaScTJS3Q61v8K9X kKrndvuZw7p3jO6QblIJwA2tBZka7+VNG5ngMCGRb6EOrCpNkQcicITfYjIazIjiqgWj 2tt+fSQ49YelcVz636KtEDGEf7ZNE7vfID4kj0j5tQgi7z4AmcAXgrT9IXbM8kKbUufa AcdEnp4B0dx1rRa6rM2Rlx7Z5PzLf/05hV06qE6SH6JpOCWwfu8iCz+YVgVvQ5UEOemh 6HXw== X-Gm-Message-State: ABy/qLZVMYrNr8QBJLxaREkrKJFuvErW0q2n3kELRsp9HmZc8Y14rKaI JkT3KIiDK2bOa2VgSTM6jRpWKDGTrjCpv2iCxczYYA== X-Google-Smtp-Source: APBJJlHutTBZ7YdbQ2Nf1bPDGC4zUoFfWoltW1I+fteK80Ql59+KkLUbW7oWTOR80U0ViJSYAjTKVzxDpY85U5Xc75E= X-Received: by 2002:aa7:d143:0:b0:520:f5dd:3335 with SMTP id r3-20020aa7d143000000b00520f5dd3335mr2518956edo.41.1689775380824; Wed, 19 Jul 2023 07:03:00 -0700 (PDT) MIME-Version: 1.0 References: <20230718214802.3214-2-jspewock@iol.unh.edu> <20230718214802.3214-3-jspewock@iol.unh.edu> In-Reply-To: <20230718214802.3214-3-jspewock@iol.unh.edu> From: =?UTF-8?Q?Juraj_Linke=C5=A1?= Date: Wed, 19 Jul 2023 16:02:49 +0200 Message-ID: Subject: Re: [PATCH v10 1/1] dts: add smoke tests To: jspewock@iol.unh.edu 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: 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 Two more things and we're done. On Tue, Jul 18, 2023 at 11:49=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/linux_session.py | 3 +- > dts/framework/remote_session/os_session.py | 49 ++++- > dts/framework/remote_session/posix_session.py | 29 ++- > .../remote_session/remote/__init__.py | 10 ++ > .../remote/interactive_remote_session.py | 82 +++++++++ > .../remote/interactive_shell.py | 132 ++++++++++++++ > .../remote_session/remote/testpmd_shell.py | 49 +++++ > dts/framework/test_result.py | 21 ++- > 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 ++++++++++++ > 19 files changed, 967 insertions(+), 94 deletions(-) > create mode 100644 dts/framework/remote_session/remote/interactive_remot= e_session.py > create mode 100644 dts/framework/remote_session/remote/interactive_shell= .py > create mode 100644 dts/framework/remote_session/remote/testpmd_shell.py > create mode 100644 dts/tests/TestSuite_smoke_tests.py > > diff --git a/dts/framework/remote_session/remote/interactive_remote_sessi= on.py b/dts/framework/remote_session/remote/interactive_remote_session.py > new file mode 100644 > index 0000000000..2d94daf2a7 > --- /dev/null > +++ b/dts/framework/remote_session/remote/interactive_remote_session.py We forgot to add proper docstring to this module. > @@ -0,0 +1,82 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2023 University of New Hampshire > + > +import socket > +import traceback > + > +from paramiko import AutoAddPolicy, SSHClient, Transport # type: ignore > +from paramiko.ssh_exception import ( # type: ignore > + AuthenticationException, > + BadHostKeyException, > + NoValidConnectionsError, > + SSHException, > +) > + > +from framework.config import NodeConfiguration > +from framework.exception import SSHConnectionError > +from framework.logger import DTSLOG > + > + > +class InteractiveRemoteSession: > + hostname: str > + ip: str > + port: int > + username: str > + password: str > + _logger: DTSLOG > + _node_config: NodeConfiguration > + session: SSHClient > + _transport: Transport | None > + > + def __init__(self, node_config: NodeConfiguration, _logger: DTSLOG) = -> None: > + self._node_config =3D node_config > + self._logger =3D _logger > + self.hostname =3D node_config.hostname > + self.username =3D node_config.user > + self.password =3D node_config.password if node_config.password e= lse "" > + port =3D "22" > + self.ip =3D node_config.hostname > + if ":" in node_config.hostname: > + self.ip, port =3D node_config.hostname.split(":") > + self.port =3D int(port) > + self._logger.info( > + f"Initializing interactive connection for {self.username}@{s= elf.hostname}" > + ) > + self._connect() > + self._logger.info( > + f"Interactive connection successful for {self.username}@{sel= f.hostname}" > + ) > + > + def _connect(self) -> None: > + client =3D SSHClient() > + client.set_missing_host_key_policy(AutoAddPolicy) > + self.session =3D client > + retry_attempts =3D 10 > + for retry_attempt in range(retry_attempts): > + try: > + client.connect( > + self.ip, > + username=3Dself.username, > + port=3Dself.port, > + password=3Dself.password, > + timeout=3D20 if self.port else 10, > + ) > + except (TypeError, BadHostKeyException, AuthenticationExcept= ion) as e: > + self._logger.exception(e) > + raise SSHConnectionError(self.hostname) from e > + except (NoValidConnectionsError, socket.error, SSHException)= as e: > + self._logger.debug(traceback.format_exc()) > + self._logger.warning(e) > + self._logger.info( > + "Retrying interactive session connection: " > + f"retry number {retry_attempt +1}" > + ) > + else: > + break > + else: > + raise SSHConnectionError(self.hostname) > + # Interactive sessions are used on an "as needed" basis so we ha= ve > + # to set a keepalive > + self._transport =3D self.session.get_transport() > + if self._transport is not None: > + self._transport.set_keepalive(30) > diff --git a/dts/framework/remote_session/remote/interactive_shell.py b/d= ts/framework/remote_session/remote/interactive_shell.py > new file mode 100644 > index 0000000000..0a1be4071f > --- /dev/null > +++ b/dts/framework/remote_session/remote/interactive_shell.py > @@ -0,0 +1,132 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2023 University of New Hampshire > + > +"""Common functionality for interactive shell handling. > + > +This base class, InteractiveShell, is meant to be extended by other clas= ses that > +contain functionality specific to that shell type. These derived classes= will often > +modify things like the prompt to expect or the arguments to pass into th= e application, > +but still utilize the same method for sending a command and collecting o= utput. How > +this output is handled however is often application specific. If an appl= ication needs > +elevated privileges to start it is expected that the method for gaining = those > +privileges is provided when initializing the class. > +""" > + > +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: > + """The base class for managing interactive shells. > + > + This class shouldn't be instantiated directly, but instead be extend= ed. It contains I agree it shouldn't be instantiated, so let's make it an abstract class (just like RemoteSession). It won't have any effect (there aren't any abstract methods or properties), but at least it'll be marked as abstract. That reminds me I need to make Node abstract in my patch as well. > + methods for starting interactive shells as well as sending commands = to these shells > + and collecting input until reaching a certain prompt. All interactiv= e applications > + will use the same SSH connection, but each will create their own cha= nnel on that > + session. > + > + Arguments: > + interactive_session: The SSH session dedicated to interactive sh= ells. > + logger: Logger used for displaying information in the console. > + get_privileged_command: Method for modifying a command to allow = it to use > + elevated privileges. If this is None, the application will n= ot be started > + with elevated privileges. > + app_args: Command line arguments to be passed to the application= on startup. > + timeout: Timeout used for the SSH channel that is dedicated to t= his interactive > + shell. This timeout is for collecting output, so if reading = from the buffer > + and no output is gathered within the timeout, an exception i= s thrown. > + > + Attributes > + _default_prompt: Prompt to expect at the end of output when send= ing a command. > + This is often overridden by derived classes. > + _command_extra_chars: Extra characters to add to the end of ever= y command > + before sending them. This is often overridden by derived cla= sses and is > + most commonly an additional newline character. > + path: Path to the executable to start the interactive applicatio= n. > + dpdk_app: Whether this application is a DPDK app. If it is, the = build > + directory for DPDK on the node will be prepended to the path= to the > + executable. > + """ > + > + _interactive_session: SSHClient > + _stdin: channel.ChannelStdinFile > + _stdout: channel.ChannelFile > + _ssh_channel: Channel > + _logger: DTSLOG > + _timeout: float > + _app_args: str > + _default_prompt: str =3D "" > + _command_extra_chars: str =3D "" > + path: PurePath > + dpdk_app: bool =3D False > + > + def __init__( > + self, > + interactive_session: SSHClient, > + logger: DTSLOG, > + get_privileged_command: Callable[[str], str] | None, > + 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 an= d stderr streams > + self._logger =3D logger > + self._timeout =3D timeout > + self._app_args =3D app_args > + self._start_application(get_privileged_command) > + > + def _start_application( > + self, get_privileged_command: Callable[[str], str] | None > + ) -> None: > + """Starts a new interactive application based on the path to the= app. > + > + This method is often overridden by subclasses as their process f= or > + starting may look different. > + """ > + start_command =3D f"{self.path} {self._app_args}" > + if get_privileged_command is not None: > + start_command =3D get_privileged_command(start_command) > + 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 ending = string. > + > + Lines that expect input are not included in the stdout buffer, s= o they cannot > + be used for expect. For example, if you were prompted to log int= o something > + with a username and password, you cannot expect "username:" beca= use it won't > + yet be in the stdout buffer. A workaround for this could be cons= uming an > + extra newline character to force the current prompt into the std= out buffer. > + > + Returns: > + All output in the buffer before expected string > + """ > + self._logger.info(f"Sending: '{command}'") > + 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()