UTI: Export results
[csit.git] / resources / libraries / python / ssh.py
index 5359a6e..e47272f 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Cisco and/or its affiliates.
+# Copyright (c) 2021 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
@@ -17,7 +17,7 @@
 import socket
 
 from io import StringIO
-from time import time, sleep
+from time import monotonic, sleep
 
 from paramiko import RSAKey, SSHClient, AutoAddPolicy
 from paramiko.ssh_exception import SSHException, NoValidConnectionsError
@@ -25,6 +25,9 @@ from robot.api import logger
 from scp import SCPClient, SCPException
 
 from resources.libraries.python.OptionString import OptionString
+from resources.libraries.python.model.ExportLog import (
+    export_ssh_command, export_ssh_result, export_ssh_timeout
+)
 
 __all__ = [
     u"exec_cmd", u"exec_cmd_no_error", u"SSH", u"SSHTimeout", u"scp_node"
@@ -82,7 +85,7 @@ class SSH:
                     raise IOError(f"Cannot connect to {node['host']}")
         else:
             try:
-                start = time()
+                start = monotonic()
                 pkey = None
                 if u"priv_key" in node:
                     pkey = RSAKey.from_private_key(StringIO(node[u"priv_key"]))
@@ -101,7 +104,7 @@ class SSH:
                 SSH.__existing_connections[node_hash] = self._ssh
                 logger.debug(
                     f"New SSH to {self._ssh.get_transport().getpeername()} "
-                    f"took {time() - start} seconds: {self._ssh}"
+                    f"took {monotonic() - start} seconds: {self._ssh}"
                 )
             except SSHException as exc:
                 raise IOError(f"Cannot connect to {node[u'host']}") from exc
@@ -142,7 +145,7 @@ class SSH:
             f"Reconnecting peer done: {node[u'host']}, {node[u'port']}"
         )
 
-    def exec_command(self, cmd, timeout=10, log_stdout_err=True):
+    def exec_command(self, cmd, timeout=10, log_stdout_err=True, export=True):
         """Execute SSH command on a new channel on the connected Node.
 
         :param cmd: Command to run on the Node.
@@ -151,9 +154,12 @@ class SSH:
         :param log_stdout_err: If True, stdout and stderr are logged. stdout
             and stderr are logged also if the return code is not zero
             independently of the value of log_stdout_err.
+        :param export: If false, do not attempt JSON export.
+            Needed for calls outside Robot (e.g. from reservation script).
         :type cmd: str or OptionString
         :type timeout: int
         :type log_stdout_err: bool
+        :type export: bool
         :returns: return_code, stdout, stderr
         :rtype: tuple(int, str, str)
         :raises SSHTimeout: If command is not finished in timeout time.
@@ -174,7 +180,9 @@ class SSH:
 
         logger.trace(f"exec_command on {peer} with timeout {timeout}: {cmd}")
 
-        start = time()
+        if export:
+            export_ssh_command(self._node[u"host"], self._node[u"port"], cmd)
+        start = monotonic()
         chan.exec_command(cmd)
         while not chan.exit_status_ready() and timeout is not None:
             if chan.recv_ready():
@@ -187,7 +195,16 @@ class SSH:
                 stderr += s_err.decode(encoding=u'utf-8', errors=u'ignore') \
                     if isinstance(s_err, bytes) else s_err
 
-            if time() - start > timeout:
+            duration = monotonic() - start
+            if duration > timeout:
+                if export:
+                    export_ssh_timeout(
+                        host=self._node[u"host"],
+                        port=self._node[u"port"],
+                        stdout=stdout,
+                        stderr=stderr,
+                        duration=duration,
+                    )
                 raise SSHTimeout(
                     f"Timeout exception during execution of command: {cmd}\n"
                     f"Current contents of stdout buffer: "
@@ -209,8 +226,8 @@ class SSH:
             stderr += s_err.decode(encoding=u'utf-8', errors=u'ignore') \
                 if isinstance(s_err, bytes) else s_err
 
-        end = time()
-        logger.trace(f"exec_command on {peer} took {end-start} seconds")
+        duration = monotonic() - start
+        logger.trace(f"exec_command on {peer} took {duration} seconds")
 
         logger.trace(f"return RC {return_code}")
         if log_stdout_err or int(return_code):
@@ -220,20 +237,33 @@ class SSH:
             logger.trace(
                 f"return STDERR {stderr}"
             )
+        if export:
+            export_ssh_result(
+                host=self._node[u"host"],
+                port=self._node[u"port"],
+                code=return_code,
+                stdout=stdout,
+                stderr=stderr,
+                duration=duration,
+            )
         return return_code, stdout, stderr
 
     def exec_command_sudo(
-            self, cmd, cmd_input=None, timeout=30, log_stdout_err=True):
+            self, cmd, cmd_input=None, timeout=30, log_stdout_err=True,
+            export=True):
         """Execute SSH command with sudo on a new channel on the connected Node.
 
         :param cmd: Command to be executed.
         :param cmd_input: Input redirected to the command.
         :param timeout: Timeout.
         :param log_stdout_err: If True, stdout and stderr are logged.
+        :param export: If false, do not attempt JSON export.
+            Needed for calls outside Robot (e.g. from reservation script).
         :type cmd: str
         :type cmd_input: str
         :type timeout: int
         :type log_stdout_err: bool
+        :type export: bool
         :returns: return_code, stdout, stderr
         :rtype: tuple(int, str, str)
 
@@ -254,7 +284,7 @@ class SSH:
         else:
             command = f"sudo -E -S {cmd} <<< \"{cmd_input}\""
         return self.exec_command(
-            command, timeout, log_stdout_err=log_stdout_err
+            command, timeout, log_stdout_err=log_stdout_err, export=export
         )
 
     def exec_command_lxc(
@@ -302,10 +332,11 @@ class SSH:
         buf = u""
         while not buf.endswith((u":~# ", u":~$ ", u"~]$ ", u"~]# ")):
             try:
-                chunk = chan.recv(self.__MAX_RECV_BUF)
-                if not chunk:
+                s_out = chan.recv(self.__MAX_RECV_BUF)
+                if not s_out:
                     break
-                buf += chunk
+                buf += s_out.decode(encoding=u'utf-8', errors=u'ignore') \
+                    if isinstance(s_out, bytes) else s_out
                 if chan.exit_status_ready():
                     logger.error(u"Channel exit status ready")
                     break
@@ -321,7 +352,7 @@ class SSH:
         :param chan: SSH channel with opened terminal.
         :param cmd: Command to be executed.
         :param prompt: Command prompt, sequence of characters used to
-        indicate readiness to accept commands.
+            indicate readiness to accept commands.
         :returns: Command output.
 
         .. warning:: Interruptingcow is used here, and it uses
@@ -335,10 +366,11 @@ class SSH:
         buf = u""
         while not buf.endswith(prompt):
             try:
-                chunk = chan.recv(self.__MAX_RECV_BUF)
-                if not chunk:
+                s_out = chan.recv(self.__MAX_RECV_BUF)
+                if not s_out:
                     break
-                buf += chunk
+                buf += s_out.decode(encoding=u'utf-8', errors=u'ignore') \
+                    if isinstance(s_out, bytes) else s_out
                 if chan.exit_status_ready():
                     logger.error(u"Channel exit status ready")
                     break
@@ -368,9 +400,9 @@ class SSH:
         connect() method has to be called first!
 
         :param local_path: Path to local file that should be uploaded; or
-        path where to save remote file.
+            path where to save remote file.
         :param remote_path: Remote path where to place uploaded file; or
-        path to remote file which should be downloaded.
+            path to remote file which should be downloaded.
         :param get: scp operation to perform. Default is put.
         :param timeout: Timeout value in seconds.
         :param wildcard: If path has wildcard characters. Default is false.
@@ -398,17 +430,20 @@ class SSH:
                 self._ssh.get_transport(), sanitize=lambda x: x,
                 socket_timeout=timeout
             )
-        start = time()
+        start = monotonic()
         if not get:
             scp.put(local_path, remote_path)
         else:
             scp.get(remote_path, local_path)
         scp.close()
-        end = time()
-        logger.trace(f"SCP took {end-start} seconds")
+        duration = monotonic() - start
+        logger.trace(f"SCP took {duration} seconds")
 
 
-def exec_cmd(node, cmd, timeout=600, sudo=False, disconnect=False):
+def exec_cmd(
+        node, cmd, timeout=600, sudo=False, disconnect=False,
+        log_stdout_err=True, export=True
+    ):
     """Convenience function to ssh/exec/return rc, out & err.
 
     Returns (rc, stdout, stderr).
@@ -418,13 +453,20 @@ def exec_cmd(node, cmd, timeout=600, sudo=False, disconnect=False):
     :param timeout: Timeout value in seconds. Default: 600.
     :param sudo: Sudo privilege execution flag. Default: False.
     :param disconnect: Close the opened SSH connection if True.
+    :param log_stdout_err: If True, stdout and stderr are logged. stdout
+        and stderr are logged also if the return code is not zero
+        independently of the value of log_stdout_err.
+    :param export: If false, do not attempt JSON export.
+        Needed for calls outside Robot (e.g. from reservation script).
     :type node: dict
     :type cmd: str or OptionString
     :type timeout: int
     :type sudo: bool
     :type disconnect: bool
+    :type log_stdout_err: bool
+    :type export: bool
     :returns: RC, Stdout, Stderr.
-    :rtype: tuple(int, str, str)
+    :rtype: Tuple[int, str, str]
     """
     if node is None:
         raise TypeError(u"Node parameter is None")
@@ -443,10 +485,14 @@ def exec_cmd(node, cmd, timeout=600, sudo=False, disconnect=False):
 
     try:
         if not sudo:
-            ret_code, stdout, stderr = ssh.exec_command(cmd, timeout=timeout)
+            ret_code, stdout, stderr = ssh.exec_command(
+                cmd, timeout=timeout, log_stdout_err=log_stdout_err,
+                export=export
+            )
         else:
             ret_code, stdout, stderr = ssh.exec_command_sudo(
-                cmd, timeout=timeout
+                cmd, timeout=timeout, log_stdout_err=log_stdout_err,
+                export=export
             )
     except SSHException as err:
         logger.error(repr(err))
@@ -460,7 +506,8 @@ def exec_cmd(node, cmd, timeout=600, sudo=False, disconnect=False):
 
 def exec_cmd_no_error(
         node, cmd, timeout=600, sudo=False, message=None, disconnect=False,
-        retries=0, include_reason=False):
+        retries=0, include_reason=False, log_stdout_err=True, export=True
+    ):
     """Convenience function to ssh/exec/return out & err.
 
     Verifies that return code is zero.
@@ -476,6 +523,11 @@ def exec_cmd_no_error(
     :param disconnect: Close the opened SSH connection if True.
     :param retries: How many times to retry on failure.
     :param include_reason: Whether default info should be appended to message.
+    :param log_stdout_err: If True, stdout and stderr are logged. stdout
+        and stderr are logged also if the return code is not zero
+        independently of the value of log_stdout_err.
+    :param export: If false, do not attempt JSON export.
+        Needed for calls outside Robot thread (e.g. parallel framework setup).
     :type node: dict
     :type cmd: str or OptionString
     :type timeout: int
@@ -484,13 +536,16 @@ def exec_cmd_no_error(
     :type disconnect: bool
     :type retries: int
     :type include_reason: bool
+    :type log_stdout_err: bool
+    :type export: bool
     :returns: Stdout, Stderr.
     :rtype: tuple(str, str)
     :raises RuntimeError: If bash return code is not 0.
     """
     for _ in range(retries + 1):
         ret_code, stdout, stderr = exec_cmd(
-            node, cmd, timeout=timeout, sudo=sudo, disconnect=disconnect
+            node, cmd, timeout=timeout, sudo=sudo, disconnect=disconnect,
+            log_stdout_err=log_stdout_err, export=export
         )
         if ret_code == 0:
             break