blob: 5cc2913da6e74aa1856578a34a81f1451d07d2b4 [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 Williams20f38712022-12-08 06:18:26 -06007import re
8import socket
9import sys
10import traceback
11
12import paramiko
George Keishing09679892022-12-08 08:21:52 -060013from robot.libraries.BuiltIn import BuiltIn
14from SSHLibrary import SSHLibrary
15
George Keishing20a34c02018-09-24 11:26:04 -050016try:
17 import exceptions
18except ImportError:
George Keishing36efbc02018-12-12 10:18:23 -060019 import builtins as exceptions
Michael Walshbd1bb602017-06-30 17:01:25 -050020
George Keishinge635ddc2022-12-08 07:38:02 -060021import func_timer as ft
Patrick Williams20f38712022-12-08 06:18:26 -060022import gen_print as gp
23
George Keishing37c58c82022-12-08 07:42:54 -060024func_timer = ft.func_timer_class()
George Keishinge635ddc2022-12-08 07:38:02 -060025
Michael Walshbd1bb602017-06-30 17:01:25 -050026sshlib = SSHLibrary()
27
28
Patrick Williams20f38712022-12-08 06:18:26 -060029def sprint_connection(connection, indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050030 r"""
31 sprint data from the connection object to a string and return it.
32
Michael Walsh410b1782019-10-22 15:56:18 -050033 connection A connection object which is created by the SSHlibrary open_connection()
34 function.
35 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050036 """
37
38 buffer = gp.sindent("", indent)
39 buffer += "connection:\n"
40 indent += 2
41 buffer += gp.sprint_varx("index", connection.index, 0, indent)
42 buffer += gp.sprint_varx("host", connection.host, 0, indent)
43 buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
44 buffer += gp.sprint_varx("port", connection.port, 0, indent)
45 buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
46 buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
47 buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
48 buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
49 buffer += gp.sprint_varx("width", connection.width, 0, indent)
50 buffer += gp.sprint_varx("height", connection.height, 0, indent)
Patrick Williams20f38712022-12-08 06:18:26 -060051 buffer += gp.sprint_varx(
52 "path_separator", connection.path_separator, 0, indent
53 )
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
Patrick Williams20f38712022-12-08 06:18:26 -060059def sprint_connections(connections=None, indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050060 r"""
61 sprint data from the connections list to a string and return it.
62
Michael Walsh410b1782019-10-22 15:56:18 -050063 connections A list of connection objects which are created by the SSHlibrary
64 open_connection function. If this value is null, this function will
65 populate with a call to the SSHlibrary get_connections() function.
66 indent The number of characters to indent the output.
Michael Walshbd1bb602017-06-30 17:01:25 -050067 """
68
69 if connections is None:
70 connections = sshlib.get_connections()
71
72 buffer = ""
73 for connection in connections:
74 buffer += sprint_connection(connection, indent)
75
76 return buffer
77
Michael Walshbd1bb602017-06-30 17:01:25 -050078
Michael Walshbd1bb602017-06-30 17:01:25 -050079def find_connection(open_connection_args={}):
Michael Walshbd1bb602017-06-30 17:01:25 -050080 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050081 Find connection that matches the given connection arguments and return connection object. Return False
82 if no matching connection is found.
Michael Walshbd1bb602017-06-30 17:01:25 -050083
84 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -050085 open_connection_args A dictionary of arg names and values which are legal to pass to the
86 SSHLibrary open_connection function as parms/args. For a match to occur,
87 the value for each item in open_connection_args must match the
88 corresponding value in the connection being examined.
Michael Walshbd1bb602017-06-30 17:01:25 -050089 """
90
91 global sshlib
92
93 for connection in sshlib.get_connections():
94 # Create connection_dict from connection object.
Patrick Williams20f38712022-12-08 06:18:26 -060095 connection_dict = dict(
96 (key, str(value)) for key, value in connection._config.items()
97 )
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
Patrick Williams20f38712022-12-08 06:18:26 -0600104def login_ssh(login_args={}, max_login_attempts=5):
Michael Walshbd1bb602017-06-30 17:01:25 -0500105 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500106 Login on the latest open SSH connection. Retry on failure up to max_login_attempts.
Michael Walshbd1bb602017-06-30 17:01:25 -0500107
108 The caller is responsible for making sure there is an open SSH connection.
109
110 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500111 login_args A dictionary containing the key/value pairs which are acceptable to the
112 SSHLibrary login function as parms/args. At a minimum, this should
113 contain a 'username' and a 'password' entry.
114 max_login_attempts The max number of times to try logging in (in the event of login
115 failures).
Michael Walshbd1bb602017-06-30 17:01:25 -0500116 """
117
Michael Walsha03a3312017-12-01 16:47:44 -0600118 gp.lprint_executing()
119
Michael Walshbd1bb602017-06-30 17:01:25 -0500120 global sshlib
121
122 # Get connection data for debug output.
123 connection = sshlib.get_connection()
Michael Walsha03a3312017-12-01 16:47:44 -0600124 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500125 for login_attempt_num in range(1, max_login_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600126 gp.lprint_timen("Logging in to " + connection.host + ".")
127 gp.lprint_var(login_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500128 try:
129 out_buf = sshlib.login(**login_args)
George Keishingdbebf272022-05-16 04:03:07 -0500130 BuiltIn().log_to_console(out_buf)
Michael Walsh4344ac22019-08-21 16:14:10 -0500131 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500132 # Login will sometimes fail if the connection is new.
133 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600134 gp.lprint_var(except_type)
135 gp.lprint_varx("except_value", str(except_value))
Patrick Williams20f38712022-12-08 06:18:26 -0600136 if (
137 except_type is paramiko.ssh_exception.SSHException
138 and re.match(r"No existing session", str(except_value))
139 ):
Michael Walshbd1bb602017-06-30 17:01:25 -0500140 continue
141 else:
Michael Walsh410b1782019-10-22 15:56:18 -0500142 # We don't tolerate any other error so break from loop and re-raise exception.
Michael Walshbd1bb602017-06-30 17:01:25 -0500143 break
144 # If we get to this point, the login has worked and we can return.
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500145 gp.lprint_var(out_buf)
Michael Walshbd1bb602017-06-30 17:01:25 -0500146 return
147
Michael Walsh410b1782019-10-22 15:56:18 -0500148 # 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 -0500149 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500150
Michael Walshbd1bb602017-06-30 17:01:25 -0500151
Patrick Williams20f38712022-12-08 06:18:26 -0600152def execute_ssh_command(
153 cmd_buf,
154 open_connection_args={},
155 login_args={},
156 print_out=0,
157 print_err=0,
158 ignore_err=1,
159 fork=0,
160 quiet=None,
161 test_mode=None,
162 time_out=None,
163):
Michael Walshbd1bb602017-06-30 17:01:25 -0500164 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500165 Run the given command in an SSH session and return the stdout, stderr and the return code.
Michael Walshbd1bb602017-06-30 17:01:25 -0500166
Michael Walsh410b1782019-10-22 15:56:18 -0500167 If there is no open SSH connection, this function will connect and login. Likewise, if the caller has
168 not yet logged in to the connection, this function will do the login.
Michael Walshbd1bb602017-06-30 17:01:25 -0500169
Michael Walsh410b1782019-10-22 15:56:18 -0500170 NOTE: There is special handling when open_connection_args['alias'] equals "device_connection".
Michael Walsh8243ac92018-05-02 13:47:07 -0500171 - A write, rather than an execute_command, is done.
172 - Only stdout is returned (no stderr or rc).
173 - print_err, ignore_err and fork are not supported.
174
Michael Walshbd1bb602017-06-30 17:01:25 -0500175 Description of arguments:
Michael Walsh410b1782019-10-22 15:56:18 -0500176 cmd_buf The command string to be run in an SSH session.
177 open_connection_args A dictionary of arg names and values which are legal to pass to the
178 SSHLibrary open_connection function as parms/args. At a minimum, this
179 should contain a 'host' entry.
180 login_args A dictionary containing the key/value pairs which are acceptable to the
181 SSHLibrary login function as parms/args. At a minimum, this should
182 contain a 'username' and a 'password' entry.
183 print_out If this is set, this function will print the stdout/stderr generated by
184 the shell command.
185 print_err If show_err is set, this function will print a standardized error report
186 if the shell command returns non-zero.
187 ignore_err Indicates that errors encountered on the sshlib.execute_command are to be
188 ignored.
189 fork Indicates that sshlib.start is to be used rather than
190 sshlib.execute_command.
191 quiet Indicates whether this function should run the pissuing() function which
192 prints an "Issuing: <cmd string>" to stdout. This defaults to the global
193 quiet value.
194 test_mode If test_mode is set, this function will not actually run the command.
195 This defaults to the global test_mode value.
196 time_out The amount of time to allow for the execution of cmd_buf. A value of
197 None means that there is no limit to how long the command may take.
Michael Walshbd1bb602017-06-30 17:01:25 -0500198 """
199
Michael Walsha03a3312017-12-01 16:47:44 -0600200 gp.lprint_executing()
Michael Walshbd1bb602017-06-30 17:01:25 -0500201
202 # Obtain default values.
203 quiet = int(gp.get_var_value(quiet, 0))
204 test_mode = int(gp.get_var_value(test_mode, 0))
205
206 if not quiet:
207 gp.pissuing(cmd_buf, test_mode)
Michael Walsha03a3312017-12-01 16:47:44 -0600208 gp.lpissuing(cmd_buf, test_mode)
Michael Walshbd1bb602017-06-30 17:01:25 -0500209
210 if test_mode:
211 return "", "", 0
212
213 global sshlib
214
Michael Walsh019817d2018-07-24 16:00:06 -0500215 max_exec_cmd_attempts = 2
Michael Walshbd1bb602017-06-30 17:01:25 -0500216 # Look for existing SSH connection.
217 # Prepare a search connection dictionary.
218 search_connection_args = open_connection_args.copy()
219 # Remove keys that don't work well for searches.
220 search_connection_args.pop("timeout", None)
221 connection = find_connection(search_connection_args)
222 if connection:
Michael Walsha03a3312017-12-01 16:47:44 -0600223 gp.lprint_timen("Found the following existing connection:")
224 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500225 if connection.alias == "":
226 index_or_alias = connection.index
227 else:
228 index_or_alias = connection.alias
Patrick Williams20f38712022-12-08 06:18:26 -0600229 gp.lprint_timen(
230 'Switching to existing connection: "' + str(index_or_alias) + '".'
231 )
Michael Walshbd1bb602017-06-30 17:01:25 -0500232 sshlib.switch_connection(index_or_alias)
233 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600234 gp.lprint_timen("Connecting to " + open_connection_args["host"] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500235 cix = sshlib.open_connection(**open_connection_args)
Michael Walsh019817d2018-07-24 16:00:06 -0500236 try:
237 login_ssh(login_args)
Michael Walsh4344ac22019-08-21 16:14:10 -0500238 except Exception:
Michael Walsh019817d2018-07-24 16:00:06 -0500239 except_type, except_value, except_traceback = sys.exc_info()
240 rc = 1
241 stderr = str(except_value)
242 stdout = ""
243 max_exec_cmd_attempts = 0
Michael Walshbd1bb602017-06-30 17:01:25 -0500244
Michael Walshbd1bb602017-06-30 17:01:25 -0500245 for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600246 gp.lprint_var(exec_cmd_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500247 try:
248 if fork:
249 sshlib.start_command(cmd_buf)
250 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600251 if open_connection_args["alias"] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500252 stdout = sshlib.write(cmd_buf)
253 stderr = ""
254 rc = 0
255 else:
Patrick Williams20f38712022-12-08 06:18:26 -0600256 stdout, stderr, rc = func_timer.run(
257 sshlib.execute_command,
258 cmd_buf,
259 return_stdout=True,
260 return_stderr=True,
261 return_rc=True,
262 time_out=time_out,
263 )
George Keishingdbebf272022-05-16 04:03:07 -0500264 BuiltIn().log_to_console(stdout)
Michael Walsh4344ac22019-08-21 16:14:10 -0500265 except Exception:
Michael Walshbd1bb602017-06-30 17:01:25 -0500266 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600267 gp.lprint_var(except_type)
268 gp.lprint_varx("except_value", str(except_value))
Michael Walsha24de032018-11-01 14:03:30 -0500269 # This may be our last time through the retry loop, so setting
270 # return variables.
271 rc = 1
272 stderr = str(except_value)
273 stdout = ""
Michael Walshbd1bb602017-06-30 17:01:25 -0500274
Patrick Williams20f38712022-12-08 06:18:26 -0600275 if except_type is exceptions.AssertionError and re.match(
276 r"Connection not open", str(except_value)
277 ):
Michael Walsh3e792622018-08-20 11:17:41 -0500278 try:
279 login_ssh(login_args)
280 # Now we must continue to next loop iteration to retry the
281 # execute_command.
282 continue
Michael Walsh4344ac22019-08-21 16:14:10 -0500283 except Exception:
Patrick Williams20f38712022-12-08 06:18:26 -0600284 (
285 except_type,
286 except_value,
287 except_traceback,
288 ) = sys.exc_info()
Michael Walsh3e792622018-08-20 11:17:41 -0500289 rc = 1
290 stderr = str(except_value)
291 stdout = ""
292 break
293
Patrick Williams20f38712022-12-08 06:18:26 -0600294 if (
295 (
296 except_type is paramiko.ssh_exception.SSHException
297 and re.match(r"SSH session not active", str(except_value))
298 )
299 or (
300 (
301 except_type is socket.error
302 or except_type is ConnectionResetError
303 )
304 and re.match(
305 r"\[Errno 104\] Connection reset by peer",
306 str(except_value),
307 )
308 )
309 or (
310 except_type is paramiko.ssh_exception.SSHException
311 and re.match(
312 r"Timeout opening channel\.", str(except_value)
313 )
314 )
315 ):
Michael Walshbd1bb602017-06-30 17:01:25 -0500316 # Close and re-open a connection.
Michael Walsh2575cd62017-11-01 17:03:45 -0500317 # Note: close_connection() doesn't appear to get rid of the
318 # connection. It merely closes it. Since there is a concern
319 # about over-consumption of resources, we use
320 # close_all_connections() which also gets rid of all
321 # connections.
Michael Walsha03a3312017-12-01 16:47:44 -0600322 gp.lprint_timen("Closing all connections.")
Michael Walsh2575cd62017-11-01 17:03:45 -0500323 sshlib.close_all_connections()
Patrick Williams20f38712022-12-08 06:18:26 -0600324 gp.lprint_timen(
325 "Connecting to " + open_connection_args["host"] + "."
326 )
Michael Walshbd1bb602017-06-30 17:01:25 -0500327 cix = sshlib.open_connection(**open_connection_args)
328 login_ssh(login_args)
329 continue
330
Michael Walsh410b1782019-10-22 15:56:18 -0500331 # We do not handle any other RuntimeErrors so we will raise the exception again.
Michael Walshe77585a2017-12-14 11:02:28 -0600332 sshlib.close_all_connections()
Michael Walshd971a7a2018-11-16 15:28:19 -0600333 gp.lprintn(traceback.format_exc())
George Keishing62246352022-08-01 01:20:06 -0500334 raise (except_value)
Michael Walshbd1bb602017-06-30 17:01:25 -0500335
336 # If we get to this point, the command was executed.
337 break
338
339 if fork:
340 return
341
342 if rc != 0 and print_err:
Michael Walsh0d5f96a2019-05-20 10:09:57 -0500343 gp.print_var(rc, gp.hexa())
Michael Walshbd1bb602017-06-30 17:01:25 -0500344 if not print_out:
345 gp.print_var(stderr)
346 gp.print_var(stdout)
347
348 if print_out:
349 gp.printn(stderr + stdout)
350
351 if not ignore_err:
Patrick Williams20f38712022-12-08 06:18:26 -0600352 message = gp.sprint_error(
353 "The prior SSH"
354 + " command returned a non-zero return"
355 + " code:\n"
356 + gp.sprint_var(rc, gp.hexa())
357 + stderr
358 + "\n"
359 )
Michael Walshbd1bb602017-06-30 17:01:25 -0500360 BuiltIn().should_be_equal(rc, 0, message)
361
Patrick Williams20f38712022-12-08 06:18:26 -0600362 if open_connection_args["alias"] == "device_connection":
Michael Walsh8243ac92018-05-02 13:47:07 -0500363 return stdout
Michael Walshbd1bb602017-06-30 17:01:25 -0500364 return stdout, stderr, rc