blob: 5cc2913da6e74aa1856578a34a81f1451d07d2b4 [file] [log] [blame]
#!/usr/bin/env python3
r"""
This module provides many valuable ssh functions such as sprint_connection, execute_ssh_command, etc.
"""
import re
import socket
import sys
import traceback
import paramiko
from robot.libraries.BuiltIn import BuiltIn
from SSHLibrary import SSHLibrary
try:
import exceptions
except ImportError:
import builtins as exceptions
import func_timer as ft
import gen_print as gp
func_timer = ft.func_timer_class()
sshlib = SSHLibrary()
def sprint_connection(connection, indent=0):
r"""
sprint data from the connection object to a string and return it.
connection A connection object which is created by the SSHlibrary open_connection()
function.
indent The number of characters to indent the output.
"""
buffer = gp.sindent("", indent)
buffer += "connection:\n"
indent += 2
buffer += gp.sprint_varx("index", connection.index, 0, indent)
buffer += gp.sprint_varx("host", connection.host, 0, indent)
buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
buffer += gp.sprint_varx("port", connection.port, 0, indent)
buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
buffer += gp.sprint_varx("width", connection.width, 0, indent)
buffer += gp.sprint_varx("height", connection.height, 0, indent)
buffer += gp.sprint_varx(
"path_separator", connection.path_separator, 0, indent
)
buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
return buffer
def sprint_connections(connections=None, indent=0):
r"""
sprint data from the connections list to a string and return it.
connections A list of connection objects which are created by the SSHlibrary
open_connection function. If this value is null, this function will
populate with a call to the SSHlibrary get_connections() function.
indent The number of characters to indent the output.
"""
if connections is None:
connections = sshlib.get_connections()
buffer = ""
for connection in connections:
buffer += sprint_connection(connection, indent)
return buffer
def find_connection(open_connection_args={}):
r"""
Find connection that matches the given connection arguments and return connection object. Return False
if no matching connection is found.
Description of argument(s):
open_connection_args A dictionary of arg names and values which are legal to pass to the
SSHLibrary open_connection function as parms/args. For a match to occur,
the value for each item in open_connection_args must match the
corresponding value in the connection being examined.
"""
global sshlib
for connection in sshlib.get_connections():
# Create connection_dict from connection object.
connection_dict = dict(
(key, str(value)) for key, value in connection._config.items()
)
if dict(connection_dict, **open_connection_args) == connection_dict:
return connection
return False
def login_ssh(login_args={}, max_login_attempts=5):
r"""
Login on the latest open SSH connection. Retry on failure up to max_login_attempts.
The caller is responsible for making sure there is an open SSH connection.
Description of argument(s):
login_args A dictionary containing the key/value pairs which are acceptable to the
SSHLibrary login function as parms/args. At a minimum, this should
contain a 'username' and a 'password' entry.
max_login_attempts The max number of times to try logging in (in the event of login
failures).
"""
gp.lprint_executing()
global sshlib
# Get connection data for debug output.
connection = sshlib.get_connection()
gp.lprintn(sprint_connection(connection))
for login_attempt_num in range(1, max_login_attempts + 1):
gp.lprint_timen("Logging in to " + connection.host + ".")
gp.lprint_var(login_attempt_num)
try:
out_buf = sshlib.login(**login_args)
BuiltIn().log_to_console(out_buf)
except Exception:
# Login will sometimes fail if the connection is new.
except_type, except_value, except_traceback = sys.exc_info()
gp.lprint_var(except_type)
gp.lprint_varx("except_value", str(except_value))
if (
except_type is paramiko.ssh_exception.SSHException
and re.match(r"No existing session", str(except_value))
):
continue
else:
# We don't tolerate any other error so break from loop and re-raise exception.
break
# If we get to this point, the login has worked and we can return.
gp.lprint_var(out_buf)
return
# If we get to this point, the login has failed on all attempts so the exception will be raised again.
raise (except_value)
def execute_ssh_command(
cmd_buf,
open_connection_args={},
login_args={},
print_out=0,
print_err=0,
ignore_err=1,
fork=0,
quiet=None,
test_mode=None,
time_out=None,
):
r"""
Run the given command in an SSH session and return the stdout, stderr and the return code.
If there is no open SSH connection, this function will connect and login. Likewise, if the caller has
not yet logged in to the connection, this function will do the login.
NOTE: There is special handling when open_connection_args['alias'] equals "device_connection".
- A write, rather than an execute_command, is done.
- Only stdout is returned (no stderr or rc).
- print_err, ignore_err and fork are not supported.
Description of arguments:
cmd_buf The command string to be run in an SSH session.
open_connection_args A dictionary of arg names and values which are legal to pass to the
SSHLibrary open_connection function as parms/args. At a minimum, this
should contain a 'host' entry.
login_args A dictionary containing the key/value pairs which are acceptable to the
SSHLibrary login function as parms/args. At a minimum, this should
contain a 'username' and a 'password' entry.
print_out If this is set, this function will print the stdout/stderr generated by
the shell command.
print_err If show_err is set, this function will print a standardized error report
if the shell command returns non-zero.
ignore_err Indicates that errors encountered on the sshlib.execute_command are to be
ignored.
fork Indicates that sshlib.start is to be used rather than
sshlib.execute_command.
quiet Indicates whether this function should run the pissuing() function which
prints an "Issuing: <cmd string>" to stdout. This defaults to the global
quiet value.
test_mode If test_mode is set, this function will not actually run the command.
This defaults to the global test_mode value.
time_out The amount of time to allow for the execution of cmd_buf. A value of
None means that there is no limit to how long the command may take.
"""
gp.lprint_executing()
# Obtain default values.
quiet = int(gp.get_var_value(quiet, 0))
test_mode = int(gp.get_var_value(test_mode, 0))
if not quiet:
gp.pissuing(cmd_buf, test_mode)
gp.lpissuing(cmd_buf, test_mode)
if test_mode:
return "", "", 0
global sshlib
max_exec_cmd_attempts = 2
# Look for existing SSH connection.
# Prepare a search connection dictionary.
search_connection_args = open_connection_args.copy()
# Remove keys that don't work well for searches.
search_connection_args.pop("timeout", None)
connection = find_connection(search_connection_args)
if connection:
gp.lprint_timen("Found the following existing connection:")
gp.lprintn(sprint_connection(connection))
if connection.alias == "":
index_or_alias = connection.index
else:
index_or_alias = connection.alias
gp.lprint_timen(
'Switching to existing connection: "' + str(index_or_alias) + '".'
)
sshlib.switch_connection(index_or_alias)
else:
gp.lprint_timen("Connecting to " + open_connection_args["host"] + ".")
cix = sshlib.open_connection(**open_connection_args)
try:
login_ssh(login_args)
except Exception:
except_type, except_value, except_traceback = sys.exc_info()
rc = 1
stderr = str(except_value)
stdout = ""
max_exec_cmd_attempts = 0
for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
gp.lprint_var(exec_cmd_attempt_num)
try:
if fork:
sshlib.start_command(cmd_buf)
else:
if open_connection_args["alias"] == "device_connection":
stdout = sshlib.write(cmd_buf)
stderr = ""
rc = 0
else:
stdout, stderr, rc = func_timer.run(
sshlib.execute_command,
cmd_buf,
return_stdout=True,
return_stderr=True,
return_rc=True,
time_out=time_out,
)
BuiltIn().log_to_console(stdout)
except Exception:
except_type, except_value, except_traceback = sys.exc_info()
gp.lprint_var(except_type)
gp.lprint_varx("except_value", str(except_value))
# This may be our last time through the retry loop, so setting
# return variables.
rc = 1
stderr = str(except_value)
stdout = ""
if except_type is exceptions.AssertionError and re.match(
r"Connection not open", str(except_value)
):
try:
login_ssh(login_args)
# Now we must continue to next loop iteration to retry the
# execute_command.
continue
except Exception:
(
except_type,
except_value,
except_traceback,
) = sys.exc_info()
rc = 1
stderr = str(except_value)
stdout = ""
break
if (
(
except_type is paramiko.ssh_exception.SSHException
and re.match(r"SSH session not active", str(except_value))
)
or (
(
except_type is socket.error
or except_type is ConnectionResetError
)
and re.match(
r"\[Errno 104\] Connection reset by peer",
str(except_value),
)
)
or (
except_type is paramiko.ssh_exception.SSHException
and re.match(
r"Timeout opening channel\.", str(except_value)
)
)
):
# Close and re-open a connection.
# Note: close_connection() doesn't appear to get rid of the
# connection. It merely closes it. Since there is a concern
# about over-consumption of resources, we use
# close_all_connections() which also gets rid of all
# connections.
gp.lprint_timen("Closing all connections.")
sshlib.close_all_connections()
gp.lprint_timen(
"Connecting to " + open_connection_args["host"] + "."
)
cix = sshlib.open_connection(**open_connection_args)
login_ssh(login_args)
continue
# We do not handle any other RuntimeErrors so we will raise the exception again.
sshlib.close_all_connections()
gp.lprintn(traceback.format_exc())
raise (except_value)
# If we get to this point, the command was executed.
break
if fork:
return
if rc != 0 and print_err:
gp.print_var(rc, gp.hexa())
if not print_out:
gp.print_var(stderr)
gp.print_var(stdout)
if print_out:
gp.printn(stderr + stdout)
if not ignore_err:
message = gp.sprint_error(
"The prior SSH"
+ " command returned a non-zero return"
+ " code:\n"
+ gp.sprint_var(rc, gp.hexa())
+ stderr
+ "\n"
)
BuiltIn().should_be_equal(rc, 0, message)
if open_connection_args["alias"] == "device_connection":
return stdout
return stdout, stderr, rc