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 E31C6A00C2; Tue, 27 Sep 2022 12:12:45 +0200 (CEST) Received: from [217.70.189.124] (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id D266C41133; Tue, 27 Sep 2022 12:12:45 +0200 (CEST) Received: from mail-lj1-f181.google.com (mail-lj1-f181.google.com [209.85.208.181]) by mails.dpdk.org (Postfix) with ESMTP id AC4B8410D0 for ; Tue, 27 Sep 2022 12:12:44 +0200 (CEST) Received: by mail-lj1-f181.google.com with SMTP id c7so10320711ljm.12 for ; Tue, 27 Sep 2022 03:12:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=semihalf.com; s=google; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date:from:to :cc:subject:date; bh=TtJ/NPLQl1h+7WkyURX9W6eRaIOwU/p0Qa1W00F6uVc=; b=pppkgwDZT88nJKCSDjHLnj0lOnL1H35ZVXR0w5Jceoe3KGKwYM+3lYGmz9c5ML0del mzrJGKTukpPrnsS4iPN7nTufv2zlfHb6v678F4jerViURschsiDVMrOJA90mHtJ+UBA4 ifUr4Wswa55QmiZz47t13hVAgZTJ7iUc3eP1ypEYcUPKSqPbuNI6w2mTy3ztj7A/6ho6 hPCWjeAOuLh7mJ9ZriIzhESVRPESKWKnFlU8DRSxh8q32DjO48/2D02mwWbZiHjcX9jP RQmVTQnbdCsgO2Qu9pz/l8WMdxZcEM5r/H0oWaWW8mczhdvlh/d4i5YCrzqLwwfzCpPD KQuQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date :x-gm-message-state:from:to:cc:subject:date; bh=TtJ/NPLQl1h+7WkyURX9W6eRaIOwU/p0Qa1W00F6uVc=; b=KnPvQfS2ys/OY71Gj1wiy+KuGVAP8e1YBYNfCzh2XonfRaYpxhWVqLOxothz2KGoQs hig/p47GWjjEYial7D5l1M3swfYNQCyituGK+wxl6yShtfCw9vFhB2jYFusUTNAwCrVx k63cfZPri1dBFut2PuQ6oZzE5btjDGP6HDCC4In5j5ckck+TPZTrG83gfz4LMemMdcme YuhL6fxwJRCiXmRJ7zqzKKaGvcCvFA7uhyP+s1ca+BfOqQwbA26grqGsx6f5TwptgizX LYaEubJrvZqeQftl2YulXO8w/h/JgwXQfoW/kTkwMwUvjYvznEHB8hTpVhvsU6vyRHdk mf8g== X-Gm-Message-State: ACrzQf1S3rbz0HuPhjmYgm1bugYlVjUM5w4IIeqbJIIHBaT/KHDeCfxA plLekF0+MF8rlTVhQrULPRndYg== X-Google-Smtp-Source: AMsMyM65VChAhCfytbBaCXfMnhXcQJLS8x9MjeAeygPKbg0NOaciSFyH4PpTNyuI6bw/tQq3UXacVA== X-Received: by 2002:a2e:2a84:0:b0:26c:57d9:10c7 with SMTP id q126-20020a2e2a84000000b0026c57d910c7mr9663433ljq.166.1664273564090; Tue, 27 Sep 2022 03:12:44 -0700 (PDT) Received: from toster (87-206-67-180.dynamic.chello.pl. [87.206.67.180]) by smtp.gmail.com with ESMTPSA id w1-20020a05651204c100b00499f700430fsm116996lfq.224.2022.09.27.03.12.43 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Sep 2022 03:12:43 -0700 (PDT) Date: Tue, 27 Sep 2022 12:12:24 +0200 From: Stanislaw Kardach To: Juraj =?utf-8?Q?Linke=C5=A1?= Cc: thomas@monjalon.net, david.marchand@redhat.com, Honnappa.Nagarahalli@arm.com, ohilyard@iol.unh.edu, lijuan.tu@intel.com, bruce.richardson@intel.com, dev@dpdk.org Subject: Re: [PATCH v5 06/10] dts: add ssh connection module Message-ID: <20220927101224.fpklwzm7dj6cxcon@toster> References: <20220926141713.2415010-1-juraj.linkes@pantheon.tech> <20220926141713.2415010-7-juraj.linkes@pantheon.tech> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <20220926141713.2415010-7-juraj.linkes@pantheon.tech> 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 Mon, Sep 26, 2022 at 02:17:09PM +0000, Juraj Linkeš wrote: > The module uses the pexpect python library and implements connection to > a node and two ways to interact with the node: > 1. Send a string with specified prompt which will be matched after > the string has been sent to the node. > 2. Send a command to be executed. No prompt is specified here. > > Signed-off-by: Owen Hilyard > Signed-off-by: Juraj Linkeš > --- > dts/framework/exception.py | 48 +++++ > .../remote_session/session_factory.py | 16 ++ > dts/framework/remote_session/ssh_session.py | 189 ++++++++++++++++++ > dts/framework/utils.py | 13 ++ > 4 files changed, 266 insertions(+) > create mode 100644 dts/framework/remote_session/session_factory.py > create mode 100644 dts/framework/remote_session/ssh_session.py > create mode 100644 dts/framework/utils.py > > diff --git a/dts/framework/exception.py b/dts/framework/exception.py > index 60fd98c9ca..8466990aa5 100644 > --- a/dts/framework/exception.py > +++ b/dts/framework/exception.py > @@ -9,6 +9,54 @@ > """ > > > +class TimeoutException(Exception): > + """ > + Command execution timeout. > + """ > + > + command: str > + output: str > + > + def __init__(self, command: str, output: str): > + self.command = command > + self.output = output > + > + def __str__(self) -> str: > + return f"TIMEOUT on {self.command}" > + > + def get_output(self) -> str: > + return self.output > + > + > +class SSHConnectionException(Exception): > + """ > + SSH connection error. > + """ > + > + host: str > + > + def __init__(self, host: str): > + self.host = host > + > + def __str__(self) -> str: > + return f"Error trying to connect with {self.host}" > + > + > +class SSHSessionDeadException(Exception): > + """ > + SSH session is not alive. > + It can no longer be used. > + """ > + > + host: str > + > + def __init__(self, host: str): > + self.host = host > + > + def __str__(self) -> str: > + return f"SSH session with {self.host} has died" > + > + > class ConfigParseException(Exception): > """ > Configuration file parse failure exception. > diff --git a/dts/framework/remote_session/session_factory.py b/dts/framework/remote_session/session_factory.py > new file mode 100644 > index 0000000000..ff05df97bf > --- /dev/null > +++ b/dts/framework/remote_session/session_factory.py > @@ -0,0 +1,16 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > +# Copyright(c) 2022 University of New Hampshire > +# > + > +from framework.config import NodeConfiguration > +from framework.logger import DTSLOG > + > +from .remote_session import RemoteSession > +from .ssh_session import SSHSession > + > + > +def create_remote_session( > + node_config: NodeConfiguration, name: str, logger: DTSLOG > +) -> RemoteSession: > + return SSHSession(node_config, name, logger) > diff --git a/dts/framework/remote_session/ssh_session.py b/dts/framework/remote_session/ssh_session.py > new file mode 100644 > index 0000000000..e0614e0f90 > --- /dev/null > +++ b/dts/framework/remote_session/ssh_session.py > @@ -0,0 +1,189 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2010-2014 Intel Corporation > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > +# Copyright(c) 2022 University of New Hampshire > +# > + > + > +import time > + > +from pexpect import pxssh > + > +from framework.config import NodeConfiguration > +from framework.exception import ( > + SSHConnectionException, > + SSHSessionDeadException, > + TimeoutException, > +) > +from framework.logger import DTSLOG > +from framework.utils import GREEN, RED > + > +from .remote_session import RemoteSession > + > + > +class SSHSession(RemoteSession): > + """ > + Module for creating Pexpect SSH sessions to a node. > + """ > + > + session: pxssh.pxssh > + magic_prompt: str > + > + def __init__( > + self, > + node_config: NodeConfiguration, > + session_name: str, > + logger: DTSLOG, > + ): > + self.magic_prompt = "MAGIC PROMPT" > + super(SSHSession, self).__init__(node_config, session_name, logger) > + > + def _connect(self) -> None: > + """ > + Create connection to assigned node. > + """ > + retry_attempts = 10 > + login_timeout = 20 if self.port else 10 > + password_regex = ( > + r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password for .+:)" > + ) > + try: > + for retry_attempt in range(retry_attempts): > + self.session = pxssh.pxssh(encoding="utf-8") > + try: > + self.session.login( > + self.ip, > + self.username, > + self.password, > + original_prompt="[$#>]", > + port=self.port, > + login_timeout=login_timeout, > + password_regex=password_regex, > + ) > + break > + except Exception as e: > + print(e) > + time.sleep(2) > + print(f"Retrying connection: retry number {retry_attempt + 1}.") > + else: > + raise Exception(f"Connection to {self.hostname} failed") > + > + self.logger.info(f"Connection to {self.hostname} succeeded") > + self.send_expect("stty -echo", "#") > + self.send_expect("stty columns 1000", "#") > + except Exception as e: > + print(RED(str(e))) > + if getattr(self, "port", None): > + suggestion = ( > + f"\nSuggestion: Check if the firewall on {self.hostname} is " > + f"stopped.\n" > + ) > + print(GREEN(suggestion)) > + > + raise SSHConnectionException(self.hostname) > + > + def send_expect_base(self, command: str, prompt: str, timeout: float) -> str: > + self.clean_session() > + original_prompt = self.session.PROMPT > + self.session.PROMPT = prompt > + self.__sendline(command) > + self.__prompt(command, timeout) > + > + before = self._get_output() > + self.session.PROMPT = original_prompt > + return before > + > + def send_expect( > + self, command: str, prompt: str, timeout: float = 15, verify: bool = False > + ) -> str | int: > + try: > + ret = self.send_expect_base(command, prompt, timeout) > + if verify: > + ret_status = self.send_expect_base("echo $?", prompt, timeout) > + try: > + retval = int(ret_status) > + if not retval: > + self.logger.error(f"Command: {command} failure!") > + self.logger.error(ret) > + return retval > + else: > + return ret Just a minor nit. Isn't the verify logic reversed in this commit? In V4 "if not retval" was an OK case (returning the output), now it reports an error. > + except ValueError: > + return ret > + else: > + return ret > + except Exception as e: > + print( > + f"Exception happened in [{command}] and output is " > + f"[{self._get_output()}]" > + ) > + raise e > + > + def _send_command(self, command: str, timeout: float = 1) -> str: > + try: > + self.clean_session() > + self.__sendline(command) > + except Exception as e: > + raise e > + > + output = self.get_output(timeout=timeout) > + self.session.PROMPT = self.session.UNIQUE_PROMPT > + self.session.prompt(0.1) > + > + return output > + > + def clean_session(self) -> None: > + self.get_output(timeout=0.01) > + > + def _get_output(self) -> str: > + if not self.is_alive(): > + raise SSHSessionDeadException(self.hostname) > + before = self.session.before.rsplit("\r\n", 1)[0] > + if before == "[PEXPECT]": > + return "" > + return before > + > + def get_output(self, timeout: float = 15) -> str: > + """ > + Get all output before timeout > + """ > + self.session.PROMPT = self.magic_prompt > + try: > + self.session.prompt(timeout) > + except Exception: > + pass > + > + before = self._get_output() > + self.__flush() > + > + self.logger.debug(before) > + return before > + > + def __flush(self) -> None: > + """ > + Clear all session buffer > + """ > + self.session.buffer = "" > + self.session.before = "" > + > + def __prompt(self, command: str, timeout: float) -> None: > + if not self.session.prompt(timeout): > + raise TimeoutException(command, self._get_output()) from None > + > + def __sendline(self, command: str) -> None: > + if not self.is_alive(): > + raise SSHSessionDeadException(self.hostname) > + if len(command) == 2 and command.startswith("^"): > + self.session.sendcontrol(command[1]) > + else: > + self.session.sendline(command) > + > + def _close(self, force: bool = False) -> None: > + if force is True: > + self.session.close() > + else: > + if self.is_alive(): > + self.session.logout() > + > + def is_alive(self) -> bool: > + return self.session.isalive() > diff --git a/dts/framework/utils.py b/dts/framework/utils.py > new file mode 100644 > index 0000000000..26b784ebb5 > --- /dev/null > +++ b/dts/framework/utils.py > @@ -0,0 +1,13 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2010-2014 Intel Corporation > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > +# Copyright(c) 2022 University of New Hampshire > +# > + > + > +def RED(text: str) -> str: > + return f"\u001B[31;1m{str(text)}\u001B[0m" > + > + > +def GREEN(text: str) -> str: > + return f"\u001B[32;1m{str(text)}\u001B[0m" > -- > 2.30.2 > Reviewed-by: Stanislaw Kardach -- Best Regards, Stanislaw Kardach