blob: b0cd049c5b02d61d2437ec85662b27357aeb2310 [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
Patrick Williams57318182022-12-08 06:18:26 -06007import sys
8import traceback
George Keishinge635ddc2022-12-08 07:38:02 -06009import re
10import socket
Michael Walshbd1bb602017-06-30 17:01:25 -050011import paramiko
George Keishing20a34c02018-09-24 11:26:04 -050012try:
13 import exceptions
14except ImportError:
George Keishing36efbc02018-12-12 10:18:23 -060015 import builtins as exceptions
Michael Walshbd1bb602017-06-30 17:01:25 -050016
Patrick Williams57318182022-12-08 06:18:26 -060017import gen_print as gp
George Keishinge635ddc2022-12-08 07:38:02 -060018import func_timer as ft
19
20
Michael Walshbd1bb602017-06-30 17:01:25 -050021from robot.libraries.BuiltIn import BuiltIn
22from SSHLibrary import SSHLibrary
Sridevi Ramesh47375aa2022-12-08 05:05:31 -060023
24func_timer = ft.func_timer_class()
25
Michael Walshbd1bb602017-06-30 17:01:25 -050026sshlib = SSHLibrary()
27
28
George Keishinge635ddc2022-12-08 07:38:02 -060029def sprint_connection(connection,
30 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050031 r"""
32 sprint data from the connection object to a string and return it.
33
Michael Walsh410b1782019-10-22 15:56:18 -050034 connection A connection object which is created by the SSHlibrary open_connection()
35 function.
36 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050037 """
38
39 buffer = gp.sindent("", indent)
40 buffer += "connection:\n"
41 indent += 2
42 buffer += gp.sprint_varx("index", connection.index, 0, indent)
43 buffer += gp.sprint_varx("host", connection.host, 0, indent)
44 buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
45 buffer += gp.sprint_varx("port", connection.port, 0, indent)
46 buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
47 buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
48 buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
49 buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
50 buffer += gp.sprint_varx("width", connection.width, 0, indent)
51 buffer += gp.sprint_varx("height", connection.height, 0, indent)
George Keishinge635ddc2022-12-08 07:38:02 -060052 buffer += gp.sprint_varx("path_separator", connection.path_separator, 0,
53 indent)
Michael Walshbd1bb602017-06-30 17:01:25 -050054 buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
55
56 return buffer
57
Michael Walshbd1bb602017-06-30 17:01:25 -050058
George Keishinge635ddc2022-12-08 07:38:02 -060059def sprint_connections(connections=None,
60 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050061 r"""
62 sprint data from the connections list to a string and return it.
63
Michael Walsh410b1782019-10-22 15:56:18 -050064 connections A list of connection objects which are created by the SSHlibrary
65 open_connection function. If this value is null, this function will
66 populate with a call to the SSHlibrary get_connections() function.
67 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050068 """
69
70 if connections is None:
71 connections = sshlib.get_connections()
72
73 buffer = ""
74 for connection in connections:
75 buffer += sprint_connection(connection, indent)
76
77 return buffer
78
Michael Walshbd1bb602017-06-30 17:01:25 -050079
Michael Walshbd1bb602017-06-30 17:01:25 -050080def find_connection(open_connection_args={}):
Michael Walshbd1bb602017-06-30 17:01:25 -050081 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050082 Find connection that matches the given connection arguments and return connection object. Return False
83 if no matching connection is found.
Michael Walshbd1bb602017-06-30 17:01:25 -050084
85 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -050086 open_connection_args A dictionary of arg names and values which are legal to pass to the
87 SSHLibrary open_connection function as parms/args. For a match to occur,
88 the value for each item in open_connection_args must match the
89 corresponding value in the connection being examined.
Michael Walshbd1bb602017-06-30 17:01:25 -050090 """
91
92 global sshlib
93
94 for connection in sshlib.get_connections():
95 # Create connection_dict from connection object.
George Keishinge635ddc2022-12-08 07:38:02 -060096 connection_dict = dict((key, str(value)) for key, value in
97 connection._config.items())
Michael Walshbd1bb602017-06-30 17:01:25 -050098 if dict(connection_dict, **open_connection_args) == connection_dict:
99 return connection
100
101 return False
102
Michael Walshbd1bb602017-06-30 17:01:25 -0500103
George Keishinge635ddc2022-12-08 07:38:02 -0600104def login_ssh(login_args={},
105 max_login_attempts=5):
Michael Walshbd1bb602017-06-30 17:01:25 -0500106 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500107 Login on the latest open SSH connection. Retry on failure up to max_login_attempts.
Michael Walshbd1bb602017-06-30 17:01:25 -0500108
109 The caller is responsible for making sure there is an open SSH connection.
110
111 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500112 login_args A dictionary containing the key/value pairs which are acceptable to the
113 SSHLibrary login function as parms/args. At a minimum, this should
114 contain a 'username' and a 'password' entry.
115 max_login_attempts The max number of times to try logging in (in the event of login
116 failures).
Michael Walshbd1bb602017-06-30 17:01:25 -0500117 """
118
Michael Walsha03a3312017-12-01 16:47:44 -0600119 gp.lprint_executing()
120
Michael Walshbd1bb602017-06-30 17:01:25 -0500121 global sshlib
122
123 # Get connection data for debug output.
124 connection = sshlib.get_connection()
Michael Walsha03a3312017-12-01 16:47:44 -0600125 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500126 for login_attempt_num in range(1, max_login_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600127 gp.lprint_timen("Logging in to " + connection.host + ".")
128 gp.lprint_var(login_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500129 try:
130 out_buf = sshlib.login(**login_args)
George Keishingdbebf272022-05-16 04:03:07 -0500131 BuiltIn().log_to_console(out_buf)
Michael Walsh4344ac22019-08-21 16:14:10 -0500132 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500133 # Login will sometimes fail if the connection is new.
134 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600135 gp.lprint_var(except_type)
136 gp.lprint_varx("except_value", str(except_value))
George Keishinge635ddc2022-12-08 07:38:02 -0600137 if except_type is paramiko.ssh_exception.SSHException and\
138 re.match(r"No existing session", str(except_value)):
Michael Walshbd1bb602017-06-30 17:01:25 -0500139 continue
140 else:
Michael Walsh410b1782019-10-22 15:56:18 -0500141 # We don't tolerate any other error so break from loop and re-raise exception.
Michael Walshbd1bb602017-06-30 17:01:25 -0500142 break
143 # If we get to this point, the login has worked and we can return.
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500144 gp.lprint_var(out_buf)
Michael Walshbd1bb602017-06-30 17:01:25 -0500145 return
146
Michael Walsh410b1782019-10-22 15:56:18 -0500147 # 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 -0500148 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500149
Michael Walshbd1bb602017-06-30 17:01:25 -0500150
George Keishinge635ddc2022-12-08 07:38:02 -0600151def execute_ssh_command(cmd_buf,
152 open_connection_args={},
153 login_args={},
154 print_out=0,
155 print_err=0,
156 ignore_err=1,
157 fork=0,
158 quiet=None,
159 test_mode=None,
160 time_out=None):
Michael Walshbd1bb602017-06-30 17:01:25 -0500161 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500162 Run the given command in an SSH session and return the stdout, stderr and the return code.
Michael Walshbd1bb602017-06-30 17:01:25 -0500163
Michael Walsh410b1782019-10-22 15:56:18 -0500164 If there is no open SSH connection, this function will connect and login. Likewise, if the caller has
165 not yet logged in to the connection, this function will do the login.
Michael Walshbd1bb602017-06-30 17:01:25 -0500166
Michael Walsh410b1782019-10-22 15:56:18 -0500167 NOTE: There is special handling when open_connection_args['alias'] equals "device_connection".
Michael Walsh8243ac92018-05-02 13:47:07 -0500168 - A write, rather than an execute_command, is done.
169 - Only stdout is returned (no stderr or rc).
170 - print_err, ignore_err and fork are not supported.
171
Michael Walshbd1bb602017-06-30 17:01:25 -0500172 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500173 cmd_buf The command string to be run in an SSH session.
174 open_connection_args A dictionary of arg names and values which are legal to pass to the
175 SSHLibrary open_connection function as parms/args. At a minimum, this
176 should contain a 'host' entry.
177 login_args A dictionary containing the key/value pairs which are acceptable to the
178 SSHLibrary login function as parms/args. At a minimum, this should
179 contain a 'username' and a 'password' entry.
180 print_out If this is set, this function will print the stdout/stderr generated by
181 the shell command.
182 print_err If show_err is set, this function will print a standardized error report
183 if the shell command returns non-zero.
184 ignore_err Indicates that errors encountered on the sshlib.execute_command are to be
185 ignored.
186 fork Indicates that sshlib.start is to be used rather than
187 sshlib.execute_command.
188 quiet Indicates whether this function should run the pissuing() function which
189 prints an "Issuing: <cmd string>" to stdout. This defaults to the global
190 quiet value.
191 test_mode If test_mode is set, this function will not actually run the command.
192 This defaults to the global test_mode value.
193 time_out The amount of time to allow for the execution of cmd_buf. A value of
194 None means that there is no limit to how long the command may take.
Michael Walshbd1bb602017-06-30 17:01:25 -0500195 """
196
Michael Walsha03a3312017-12-01 16:47:44 -0600197 gp.lprint_executing()
Michael Walshbd1bb602017-06-30 17:01:25 -0500198
199 # Obtain default values.
200 quiet = int(gp.get_var_value(quiet, 0))
201 test_mode = int(gp.get_var_value(test_mode, 0))
202
203 if not quiet:
204 gp.pissuing(cmd_buf, test_mode)
Michael Walsha03a3312017-12-01 16:47:44 -0600205 gp.lpissuing(cmd_buf, test_mode)
Michael Walshbd1bb602017-06-30 17:01:25 -0500206
207 if test_mode:
208 return "", "", 0
209
210 global sshlib
211
Michael Walsh019817d2018-07-24 16:00:06 -0500212 max_exec_cmd_attempts = 2
Michael Walshbd1bb602017-06-30 17:01:25 -0500213 # Look for existing SSH connection.
214 # Prepare a search connection dictionary.
215 search_connection_args = open_connection_args.copy()
216 # Remove keys that don't work well for searches.
217 search_connection_args.pop("timeout", None)
218 connection = find_connection(search_connection_args)
219 if connection:
Michael Walsha03a3312017-12-01 16:47:44 -0600220 gp.lprint_timen("Found the following existing connection:")
221 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500222 if connection.alias == "":
223 index_or_alias = connection.index
224 else:
225 index_or_alias = connection.alias
George Keishinge635ddc2022-12-08 07:38:02 -0600226 gp.lprint_timen("Switching to existing connection: \""
227 + str(index_or_alias) + "\".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500228 sshlib.switch_connection(index_or_alias)
229 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600230 gp.lprint_timen("Connecting to " + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500231 cix = sshlib.open_connection(**open_connection_args)
Michael Walsh019817d2018-07-24 16:00:06 -0500232 try:
233 login_ssh(login_args)
Michael Walsh4344ac22019-08-21 16:14:10 -0500234 except Exception:
Michael Walsh019817d2018-07-24 16:00:06 -0500235 except_type, except_value, except_traceback = sys.exc_info()
236 rc = 1
237 stderr = str(except_value)
238 stdout = ""
239 max_exec_cmd_attempts = 0
Michael Walshbd1bb602017-06-30 17:01:25 -0500240
Michael Walshbd1bb602017-06-30 17:01:25 -0500241 for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600242 gp.lprint_var(exec_cmd_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500243 try:
244 if fork:
245 sshlib.start_command(cmd_buf)
246 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600247 if open_connection_args['alias'] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500248 stdout = sshlib.write(cmd_buf)
249 stderr = ""
250 rc = 0
251 else:
George Keishinge635ddc2022-12-08 07:38:02 -0600252 stdout, stderr, rc = \
253 func_timer.run(sshlib.execute_command,
254 cmd_buf,
255 return_stdout=True,
256 return_stderr=True,
257 return_rc=True,
258 time_out=time_out)
George Keishingdbebf272022-05-16 04:03:07 -0500259 BuiltIn().log_to_console(stdout)
Michael Walsh4344ac22019-08-21 16:14:10 -0500260 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500261 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600262 gp.lprint_var(except_type)
263 gp.lprint_varx("except_value", str(except_value))
Michael Walsha24de032018-11-01 14:03:30 -0500264 # This may be our last time through the retry loop, so setting
265 # return variables.
266 rc = 1
267 stderr = str(except_value)
268 stdout = ""
Michael Walshbd1bb602017-06-30 17:01:25 -0500269
George Keishinge635ddc2022-12-08 07:38:02 -0600270 if except_type is exceptions.AssertionError and\
271 re.match(r"Connection not open", str(except_value)):
Michael Walsh3e792622018-08-20 11:17:41 -0500272 try:
273 login_ssh(login_args)
274 # Now we must continue to next loop iteration to retry the
275 # execute_command.
276 continue
Michael Walsh4344ac22019-08-21 16:14:10 -0500277 except Exception:
George Keishinge635ddc2022-12-08 07:38:02 -0600278 except_type, except_value, except_traceback =\
279 sys.exc_info()
Michael Walsh3e792622018-08-20 11:17:41 -0500280 rc = 1
281 stderr = str(except_value)
282 stdout = ""
283 break
284
George Keishinge635ddc2022-12-08 07:38:02 -0600285 if (except_type is paramiko.ssh_exception.SSHException
286 and re.match(r"SSH session not active", str(except_value))) or\
287 ((except_type is socket.error
288 or except_type is ConnectionResetError)
289 and re.match(r"\[Errno 104\] Connection reset by peer",
290 str(except_value))) or\
291 (except_type is paramiko.ssh_exception.SSHException
292 and re.match(r"Timeout opening channel\.",
293 str(except_value))):
Michael Walshbd1bb602017-06-30 17:01:25 -0500294 # Close and re-open a connection.
Michael Walsh2575cd62017-11-01 17:03:45 -0500295 # Note: close_connection() doesn't appear to get rid of the
296 # connection. It merely closes it. Since there is a concern
297 # about over-consumption of resources, we use
298 # close_all_connections() which also gets rid of all
299 # connections.
Michael Walsha03a3312017-12-01 16:47:44 -0600300 gp.lprint_timen("Closing all connections.")
Michael Walsh2575cd62017-11-01 17:03:45 -0500301 sshlib.close_all_connections()
George Keishinge635ddc2022-12-08 07:38:02 -0600302 gp.lprint_timen("Connecting to "
303 + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500304 cix = sshlib.open_connection(**open_connection_args)
305 login_ssh(login_args)
306 continue
307
Michael Walsh410b1782019-10-22 15:56:18 -0500308 # We do not handle any other RuntimeErrors so we will raise the exception again.
Michael Walshe77585a2017-12-14 11:02:28 -0600309 sshlib.close_all_connections()
Michael Walshd971a7a2018-11-16 15:28:19 -0600310 gp.lprintn(traceback.format_exc())
George Keishing62246352022-08-01 01:20:06 -0500311 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500312
313 # If we get to this point, the command was executed.
314 break
315
316 if fork:
317 return
318
319 if rc != 0 and print_err:
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500320 gp.print_var(rc, gp.hexa())
Michael Walshbd1bb602017-06-30 17:01:25 -0500321 if not print_out:
322 gp.print_var(stderr)
323 gp.print_var(stdout)
324
325 if print_out:
326 gp.printn(stderr + stdout)
327
328 if not ignore_err:
George Keishinge635ddc2022-12-08 07:38:02 -0600329 message = gp.sprint_error("The prior SSH"
330 + " command returned a non-zero return"
331 + " code:\n"
332 + gp.sprint_var(rc, gp.hexa()) + stderr
333 + "\n")
Michael Walshbd1bb602017-06-30 17:01:25 -0500334 BuiltIn().should_be_equal(rc, 0, message)
335
George Keishinge635ddc2022-12-08 07:38:02 -0600336 if open_connection_args['alias'] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500337 return stdout
Michael Walshbd1bb602017-06-30 17:01:25 -0500338 return stdout, stderr, rc