[v1,1/8] dts: add ssh pexpect library

Message ID 20220622121448.3304251-2-juraj.linkes@pantheon.tech (mailing list archive)
State Superseded, archived
Delegated to: Thomas Monjalon
Headers
Series dts: ssh connection to a node |

Checks

Context Check Description
ci/checkpatch success coding style OK

Commit Message

Juraj Linkeš June 22, 2022, 12:14 p.m. UTC
  The library 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: Juraj Linkeš <juraj.linkes@pantheon.tech>
---
 dts/framework/exception.py   |  48 +++++++++
 dts/framework/ssh_pexpect.py | 185 +++++++++++++++++++++++++++++++++++
 dts/framework/utils.py       |  11 +++
 3 files changed, 244 insertions(+)
 create mode 100644 dts/framework/exception.py
 create mode 100644 dts/framework/ssh_pexpect.py
 create mode 100644 dts/framework/utils.py
  

Patch

diff --git a/dts/framework/exception.py b/dts/framework/exception.py
new file mode 100644
index 0000000000..a109dd1fb8
--- /dev/null
+++ b/dts/framework/exception.py
@@ -0,0 +1,48 @@ 
+"""
+User-defined exceptions used across the framework.
+"""
+
+
+class TimeoutException(Exception):
+
+    """
+    Command execution timeout.
+    """
+
+    def __init__(self, command, output):
+        self.command = command
+        self.output = output
+
+    def __str__(self):
+        msg = "TIMEOUT on %s" % (self.command)
+        return msg
+
+    def get_output(self):
+        return self.output
+
+
+class SSHConnectionException(Exception):
+
+    """
+    SSH connection error.
+    """
+
+    def __init__(self, host):
+        self.host = host
+
+    def __str__(self):
+        return "Error trying to connect with %s" % self.host
+
+
+class SSHSessionDeadException(Exception):
+
+    """
+    SSH session is not alive.
+    It can no longer be used.
+    """
+
+    def __init__(self, host):
+        self.host = host
+
+    def __str__(self):
+        return "SSH session with %s has been dead" % self.host
diff --git a/dts/framework/ssh_pexpect.py b/dts/framework/ssh_pexpect.py
new file mode 100644
index 0000000000..bccc6fae94
--- /dev/null
+++ b/dts/framework/ssh_pexpect.py
@@ -0,0 +1,185 @@ 
+import time
+
+from pexpect import pxssh
+
+from .exception import SSHConnectionException, SSHSessionDeadException, TimeoutException
+from .utils import GREEN, RED
+
+"""
+Module handles ssh sessions to TG and SUT.
+Implements send_expect function to send commands and get output data.
+"""
+
+
+class SSHPexpect:
+    def __init__(self, node, username, password):
+        self.magic_prompt = "MAGIC PROMPT"
+        self.logger = None
+
+        self.node = node
+        self.username = username
+        self.password = password
+
+        self._connect_host()
+
+    def _connect_host(self):
+        """
+        Create connection to assigned node.
+        """
+        retry_times = 10
+        try:
+            if ":" in self.node:
+                while retry_times:
+                    self.ip = self.node.split(":")[0]
+                    self.port = int(self.node.split(":")[1])
+                    self.session = pxssh.pxssh(encoding="utf-8")
+                    try:
+                        self.session.login(
+                            self.ip,
+                            self.username,
+                            self.password,
+                            original_prompt="[$#>]",
+                            port=self.port,
+                            login_timeout=20,
+                            password_regex=r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password for .+:)",
+                        )
+                    except Exception as e:
+                        print(e)
+                        time.sleep(2)
+                        retry_times -= 1
+                        print("retry %d times connecting..." % (10 - retry_times))
+                    else:
+                        break
+                else:
+                    raise Exception("connect to %s:%s failed" % (self.ip, self.port))
+            else:
+                self.session = pxssh.pxssh(encoding="utf-8")
+                self.session.login(
+                    self.node,
+                    self.username,
+                    self.password,
+                    original_prompt="[$#>]",
+                    password_regex=r"(?i)(?:password:)|(?:passphrase for key)|(?i)(password for .+:)",
+                )
+            self.send_expect("stty -echo", "#")
+            self.send_expect("stty columns 1000", "#")
+        except Exception as e:
+            print(RED(e))
+            if getattr(self, "port", None):
+                suggestion = (
+                    "\nSuggession: Check if the firewall on [ %s ] " % self.ip
+                    + "is stopped\n"
+                )
+                print(GREEN(suggestion))
+
+            raise SSHConnectionException(self.node)
+
+    def init_log(self, logger):
+        self.logger = logger
+        self.logger.info("ssh %s@%s" % (self.username, self.node))
+
+    def send_expect_base(self, command, expected, timeout):
+        self.clean_session()
+        self.session.PROMPT = expected
+        self.__sendline(command)
+        self.__prompt(command, timeout)
+
+        before = self.get_output_before()
+        return before
+
+    def send_expect(self, command, expected, timeout=15, verify=False):
+
+        try:
+            ret = self.send_expect_base(command, expected, timeout)
+            if verify:
+                ret_status = self.send_expect_base("echo $?", expected, timeout)
+                if not int(ret_status):
+                    return ret
+                else:
+                    self.logger.error("Command: %s failure!" % command)
+                    self.logger.error(ret)
+                    return int(ret_status)
+            else:
+                return ret
+        except Exception as e:
+            print(
+                RED(
+                    "Exception happened in [%s] and output is [%s]"
+                    % (command, self.get_output_before())
+                )
+            )
+            raise e
+
+    def send_command(self, command, timeout=1):
+        try:
+            self.clean_session()
+            self.__sendline(command)
+        except Exception as e:
+            raise e
+
+        output = self.get_session_before(timeout=timeout)
+        self.session.PROMPT = self.session.UNIQUE_PROMPT
+        self.session.prompt(0.1)
+
+        return output
+
+    def clean_session(self):
+        self.get_session_before(timeout=0.01)
+
+    def get_session_before(self, timeout=15):
+        """
+        Get all output before timeout
+        """
+        self.session.PROMPT = self.magic_prompt
+        try:
+            self.session.prompt(timeout)
+        except Exception as e:
+            pass
+
+        before = self.get_output_all()
+        self.__flush()
+
+        return before
+
+    def __flush(self):
+        """
+        Clear all session buffer
+        """
+        self.session.buffer = ""
+        self.session.before = ""
+
+    def __prompt(self, command, timeout):
+        if not self.session.prompt(timeout):
+            raise TimeoutException(command, self.get_output_all()) from None
+
+    def __sendline(self, command):
+        if not self.isalive():
+            raise SSHSessionDeadException(self.node)
+        if len(command) == 2 and command.startswith("^"):
+            self.session.sendcontrol(command[1])
+        else:
+            self.session.sendline(command)
+
+    def get_output_before(self):
+        if not self.isalive():
+            raise SSHSessionDeadException(self.node)
+        before = self.session.before.rsplit("\r\n", 1)
+        if before[0] == "[PEXPECT]":
+            before[0] = ""
+
+        return before[0]
+
+    def get_output_all(self):
+        output = self.session.before
+        output.replace("[PEXPECT]", "")
+        return output
+
+    def close(self, force=False):
+        if force is True:
+            self.session.close()
+        else:
+            if self.isalive():
+                self.session.logout()
+
+    def isalive(self):
+        return self.session.isalive()
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
new file mode 100644
index 0000000000..0ffd992952
--- /dev/null
+++ b/dts/framework/utils.py
@@ -0,0 +1,11 @@ 
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2010-2014 Intel Corporation
+#
+
+
+def RED(text):
+    return "\x1B[" + "31;1m" + str(text) + "\x1B[" + "0m"
+
+
+def GREEN(text):
+    return "\x1B[" + "32;1m" + str(text) + "\x1B[" + "0m"