blob: b08d470d1f6cbe721840583a352055c78085fa34 [file] [log] [blame]
George Keishinge7e91712021-09-03 11:28:44 -05001#!/usr/bin/env python3
Michael Walshbd1bb602017-06-30 17:01:25 -05002
3r"""
Michael Walsh410b1782019-10-22 15:56:18 -05004This module provides many valuable ssh functions such as sprint_connection, execute_ssh_command, etc.
Michael Walshbd1bb602017-06-30 17:01:25 -05005"""
6
George Keishing09679892022-12-08 08:21:52 -06007from robot.libraries.BuiltIn import BuiltIn
8from SSHLibrary import SSHLibrary
9
Patrick Williams57318182022-12-08 06:18:26 -060010import sys
11import traceback
George Keishinge635ddc2022-12-08 07:38:02 -060012import re
13import socket
Michael Walshbd1bb602017-06-30 17:01:25 -050014import paramiko
George Keishing20a34c02018-09-24 11:26:04 -050015try:
16 import exceptions
17except ImportError:
George Keishing36efbc02018-12-12 10:18:23 -060018 import builtins as exceptions
Michael Walshbd1bb602017-06-30 17:01:25 -050019
Patrick Williams57318182022-12-08 06:18:26 -060020import gen_print as gp
George Keishinge635ddc2022-12-08 07:38:02 -060021import func_timer as ft
George Keishing37c58c82022-12-08 07:42:54 -060022func_timer = ft.func_timer_class()
George Keishinge635ddc2022-12-08 07:38:02 -060023
Michael Walshbd1bb602017-06-30 17:01:25 -050024sshlib = SSHLibrary()
25
26
George Keishinge635ddc2022-12-08 07:38:02 -060027def sprint_connection(connection,
28 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050029 r"""
30 sprint data from the connection object to a string and return it.
31
Michael Walsh410b1782019-10-22 15:56:18 -050032 connection A connection object which is created by the SSHlibrary open_connection()
33 function.
34 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050035 """
36
37 buffer = gp.sindent("", indent)
38 buffer += "connection:\n"
39 indent += 2
40 buffer += gp.sprint_varx("index", connection.index, 0, indent)
41 buffer += gp.sprint_varx("host", connection.host, 0, indent)
42 buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
43 buffer += gp.sprint_varx("port", connection.port, 0, indent)
44 buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
45 buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
46 buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
47 buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
48 buffer += gp.sprint_varx("width", connection.width, 0, indent)
49 buffer += gp.sprint_varx("height", connection.height, 0, indent)
George Keishinge635ddc2022-12-08 07:38:02 -060050 buffer += gp.sprint_varx("path_separator", connection.path_separator, 0,
51 indent)
Michael Walshbd1bb602017-06-30 17:01:25 -050052 buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
53
54 return buffer
55
Michael Walshbd1bb602017-06-30 17:01:25 -050056
George Keishinge635ddc2022-12-08 07:38:02 -060057def sprint_connections(connections=None,
58 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050059 r"""
60 sprint data from the connections list to a string and return it.
61
Michael Walsh410b1782019-10-22 15:56:18 -050062 connections A list of connection objects which are created by the SSHlibrary
63 open_connection function. If this value is null, this function will
64 populate with a call to the SSHlibrary get_connections() function.
65 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050066 """
67
68 if connections is None:
69 connections = sshlib.get_connections()
70
71 buffer = ""
72 for connection in connections:
73 buffer += sprint_connection(connection, indent)
74
75 return buffer
76
Michael Walshbd1bb602017-06-30 17:01:25 -050077
Michael Walshbd1bb602017-06-30 17:01:25 -050078def find_connection(open_connection_args={}):
Michael Walshbd1bb602017-06-30 17:01:25 -050079 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050080 Find connection that matches the given connection arguments and return connection object. Return False
81 if no matching connection is found.
Michael Walshbd1bb602017-06-30 17:01:25 -050082
83 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -050084 open_connection_args A dictionary of arg names and values which are legal to pass to the
85 SSHLibrary open_connection function as parms/args. For a match to occur,
86 the value for each item in open_connection_args must match the
87 corresponding value in the connection being examined.
Michael Walshbd1bb602017-06-30 17:01:25 -050088 """
89
90 global sshlib
91
92 for connection in sshlib.get_connections():
93 # Create connection_dict from connection object.
George Keishinge635ddc2022-12-08 07:38:02 -060094 connection_dict = dict((key, str(value)) for key, value in
95 connection._config.items())
Michael Walshbd1bb602017-06-30 17:01:25 -050096 if dict(connection_dict, **open_connection_args) == connection_dict:
97 return connection
98
99 return False
100
Michael Walshbd1bb602017-06-30 17:01:25 -0500101
George Keishinge635ddc2022-12-08 07:38:02 -0600102def login_ssh(login_args={},
103 max_login_attempts=5):
Michael Walshbd1bb602017-06-30 17:01:25 -0500104 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500105 Login on the latest open SSH connection. Retry on failure up to max_login_attempts.
Michael Walshbd1bb602017-06-30 17:01:25 -0500106
107 The caller is responsible for making sure there is an open SSH connection.
108
109 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500110 login_args A dictionary containing the key/value pairs which are acceptable to the
111 SSHLibrary login function as parms/args. At a minimum, this should
112 contain a 'username' and a 'password' entry.
113 max_login_attempts The max number of times to try logging in (in the event of login
114 failures).
Michael Walshbd1bb602017-06-30 17:01:25 -0500115 """
116
Michael Walsha03a3312017-12-01 16:47:44 -0600117 gp.lprint_executing()
118
Michael Walshbd1bb602017-06-30 17:01:25 -0500119 global sshlib
120
121 # Get connection data for debug output.
122 connection = sshlib.get_connection()
Michael Walsha03a3312017-12-01 16:47:44 -0600123 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500124 for login_attempt_num in range(1, max_login_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600125 gp.lprint_timen("Logging in to " + connection.host + ".")
126 gp.lprint_var(login_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500127 try:
128 out_buf = sshlib.login(**login_args)
George Keishingdbebf272022-05-16 04:03:07 -0500129 BuiltIn().log_to_console(out_buf)
Michael Walsh4344ac22019-08-21 16:14:10 -0500130 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500131 # Login will sometimes fail if the connection is new.
132 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600133 gp.lprint_var(except_type)
134 gp.lprint_varx("except_value", str(except_value))
George Keishinge635ddc2022-12-08 07:38:02 -0600135 if except_type is paramiko.ssh_exception.SSHException and\
136 re.match(r"No existing session", str(except_value)):
Michael Walshbd1bb602017-06-30 17:01:25 -0500137 continue
138 else:
Michael Walsh410b1782019-10-22 15:56:18 -0500139 # We don't tolerate any other error so break from loop and re-raise exception.
Michael Walshbd1bb602017-06-30 17:01:25 -0500140 break
141 # If we get to this point, the login has worked and we can return.
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500142 gp.lprint_var(out_buf)
Michael Walshbd1bb602017-06-30 17:01:25 -0500143 return
144
Michael Walsh410b1782019-10-22 15:56:18 -0500145 # If we get to this point, the login has failed on all attempts so the exception will be raised again.
George Keishing62246352022-08-01 01:20:06 -0500146 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500147
Michael Walshbd1bb602017-06-30 17:01:25 -0500148
George Keishinge635ddc2022-12-08 07:38:02 -0600149def execute_ssh_command(cmd_buf,
150 open_connection_args={},
151 login_args={},
152 print_out=0,
153 print_err=0,
154 ignore_err=1,
155 fork=0,
156 quiet=None,
157 test_mode=None,
158 time_out=None):
Michael Walshbd1bb602017-06-30 17:01:25 -0500159 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500160 Run the given command in an SSH session and return the stdout, stderr and the return code.
Michael Walshbd1bb602017-06-30 17:01:25 -0500161
Michael Walsh410b1782019-10-22 15:56:18 -0500162 If there is no open SSH connection, this function will connect and login. Likewise, if the caller has
163 not yet logged in to the connection, this function will do the login.
Michael Walshbd1bb602017-06-30 17:01:25 -0500164
Michael Walsh410b1782019-10-22 15:56:18 -0500165 NOTE: There is special handling when open_connection_args['alias'] equals "device_connection".
Michael Walsh8243ac92018-05-02 13:47:07 -0500166 - A write, rather than an execute_command, is done.
167 - Only stdout is returned (no stderr or rc).
168 - print_err, ignore_err and fork are not supported.
169
Michael Walshbd1bb602017-06-30 17:01:25 -0500170 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500171 cmd_buf The command string to be run in an SSH session.
172 open_connection_args A dictionary of arg names and values which are legal to pass to the
173 SSHLibrary open_connection function as parms/args. At a minimum, this
174 should contain a 'host' entry.
175 login_args A dictionary containing the key/value pairs which are acceptable to the
176 SSHLibrary login function as parms/args. At a minimum, this should
177 contain a 'username' and a 'password' entry.
178 print_out If this is set, this function will print the stdout/stderr generated by
179 the shell command.
180 print_err If show_err is set, this function will print a standardized error report
181 if the shell command returns non-zero.
182 ignore_err Indicates that errors encountered on the sshlib.execute_command are to be
183 ignored.
184 fork Indicates that sshlib.start is to be used rather than
185 sshlib.execute_command.
186 quiet Indicates whether this function should run the pissuing() function which
187 prints an "Issuing: <cmd string>" to stdout. This defaults to the global
188 quiet value.
189 test_mode If test_mode is set, this function will not actually run the command.
190 This defaults to the global test_mode value.
191 time_out The amount of time to allow for the execution of cmd_buf. A value of
192 None means that there is no limit to how long the command may take.
Michael Walshbd1bb602017-06-30 17:01:25 -0500193 """
194
Michael Walsha03a3312017-12-01 16:47:44 -0600195 gp.lprint_executing()
Michael Walshbd1bb602017-06-30 17:01:25 -0500196
197 # Obtain default values.
198 quiet = int(gp.get_var_value(quiet, 0))
199 test_mode = int(gp.get_var_value(test_mode, 0))
200
201 if not quiet:
202 gp.pissuing(cmd_buf, test_mode)
Michael Walsha03a3312017-12-01 16:47:44 -0600203 gp.lpissuing(cmd_buf, test_mode)
Michael Walshbd1bb602017-06-30 17:01:25 -0500204
205 if test_mode:
206 return "", "", 0
207
208 global sshlib
209
Michael Walsh019817d2018-07-24 16:00:06 -0500210 max_exec_cmd_attempts = 2
Michael Walshbd1bb602017-06-30 17:01:25 -0500211 # Look for existing SSH connection.
212 # Prepare a search connection dictionary.
213 search_connection_args = open_connection_args.copy()
214 # Remove keys that don't work well for searches.
215 search_connection_args.pop("timeout", None)
216 connection = find_connection(search_connection_args)
217 if connection:
Michael Walsha03a3312017-12-01 16:47:44 -0600218 gp.lprint_timen("Found the following existing connection:")
219 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500220 if connection.alias == "":
221 index_or_alias = connection.index
222 else:
223 index_or_alias = connection.alias
George Keishinge635ddc2022-12-08 07:38:02 -0600224 gp.lprint_timen("Switching to existing connection: \""
225 + str(index_or_alias) + "\".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500226 sshlib.switch_connection(index_or_alias)
227 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600228 gp.lprint_timen("Connecting to " + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500229 cix = sshlib.open_connection(**open_connection_args)
Michael Walsh019817d2018-07-24 16:00:06 -0500230 try:
231 login_ssh(login_args)
Michael Walsh4344ac22019-08-21 16:14:10 -0500232 except Exception:
Michael Walsh019817d2018-07-24 16:00:06 -0500233 except_type, except_value, except_traceback = sys.exc_info()
234 rc = 1
235 stderr = str(except_value)
236 stdout = ""
237 max_exec_cmd_attempts = 0
Michael Walshbd1bb602017-06-30 17:01:25 -0500238
Michael Walshbd1bb602017-06-30 17:01:25 -0500239 for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600240 gp.lprint_var(exec_cmd_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500241 try:
242 if fork:
243 sshlib.start_command(cmd_buf)
244 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600245 if open_connection_args['alias'] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500246 stdout = sshlib.write(cmd_buf)
247 stderr = ""
248 rc = 0
249 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600250 stdout, stderr, rc = \
251 func_timer.run(sshlib.execute_command,
252 cmd_buf,
253 return_stdout=True,
254 return_stderr=True,
255 return_rc=True,
256 time_out=time_out)
George Keishingdbebf272022-05-16 04:03:07 -0500257 BuiltIn().log_to_console(stdout)
Michael Walsh4344ac22019-08-21 16:14:10 -0500258 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500259 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600260 gp.lprint_var(except_type)
261 gp.lprint_varx("except_value", str(except_value))
Michael Walsha24de032018-11-01 14:03:30 -0500262 # This may be our last time through the retry loop, so setting
263 # return variables.
264 rc = 1
265 stderr = str(except_value)
266 stdout = ""
Michael Walshbd1bb602017-06-30 17:01:25 -0500267
George Keishinge635ddc2022-12-08 07:38:02 -0600268 if except_type is exceptions.AssertionError and\
269 re.match(r"Connection not open", str(except_value)):
Michael Walsh3e792622018-08-20 11:17:41 -0500270 try:
271 login_ssh(login_args)
272 # Now we must continue to next loop iteration to retry the
273 # execute_command.
274 continue
Michael Walsh4344ac22019-08-21 16:14:10 -0500275 except Exception:
George Keishinge635ddc2022-12-08 07:38:02 -0600276 except_type, except_value, except_traceback =\
277 sys.exc_info()
Michael Walsh3e792622018-08-20 11:17:41 -0500278 rc = 1
279 stderr = str(except_value)
280 stdout = ""
281 break
282
George Keishinge635ddc2022-12-08 07:38:02 -0600283 if (except_type is paramiko.ssh_exception.SSHException
284 and re.match(r"SSH session not active", str(except_value))) or\
285 ((except_type is socket.error
286 or except_type is ConnectionResetError)
287 and re.match(r"\[Errno 104\] Connection reset by peer",
288 str(except_value))) or\
289 (except_type is paramiko.ssh_exception.SSHException
290 and re.match(r"Timeout opening channel\.",
291 str(except_value))):
Michael Walshbd1bb602017-06-30 17:01:25 -0500292 # Close and re-open a connection.
Michael Walsh2575cd62017-11-01 17:03:45 -0500293 # Note: close_connection() doesn't appear to get rid of the
294 # connection. It merely closes it. Since there is a concern
295 # about over-consumption of resources, we use
296 # close_all_connections() which also gets rid of all
297 # connections.
Michael Walsha03a3312017-12-01 16:47:44 -0600298 gp.lprint_timen("Closing all connections.")
Michael Walsh2575cd62017-11-01 17:03:45 -0500299 sshlib.close_all_connections()
George Keishinge635ddc2022-12-08 07:38:02 -0600300 gp.lprint_timen("Connecting to "
301 + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500302 cix = sshlib.open_connection(**open_connection_args)
303 login_ssh(login_args)
304 continue
305
Michael Walsh410b1782019-10-22 15:56:18 -0500306 # We do not handle any other RuntimeErrors so we will raise the exception again.
Michael Walshe77585a2017-12-14 11:02:28 -0600307 sshlib.close_all_connections()
Michael Walshd971a7a2018-11-16 15:28:19 -0600308 gp.lprintn(traceback.format_exc())
George Keishing62246352022-08-01 01:20:06 -0500309 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500310
311 # If we get to this point, the command was executed.
312 break
313
314 if fork:
315 return
316
317 if rc != 0 and print_err:
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500318 gp.print_var(rc, gp.hexa())
Michael Walshbd1bb602017-06-30 17:01:25 -0500319 if not print_out:
320 gp.print_var(stderr)
321 gp.print_var(stdout)
322
323 if print_out:
324 gp.printn(stderr + stdout)
325
326 if not ignore_err:
George Keishinge635ddc2022-12-08 07:38:02 -0600327 message = gp.sprint_error("The prior SSH"
328 + " command returned a non-zero return"
329 + " code:\n"
330 + gp.sprint_var(rc, gp.hexa()) + stderr
331 + "\n")
Michael Walshbd1bb602017-06-30 17:01:25 -0500332 BuiltIn().should_be_equal(rc, 0, message)
333
George Keishinge635ddc2022-12-08 07:38:02 -0600334 if open_connection_args['alias'] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500335 return stdout
Michael Walshbd1bb602017-06-30 17:01:25 -0500336 return stdout, stderr, rc