blob: 017490887a5b1982582b58d0a1e9e9e25ffbd3ef [file] [log] [blame]
Michael Walshbd1bb602017-06-30 17:01:25 -05001#!/usr/bin/env python
2
3r"""
4This module provides many valuable ssh functions such as sprint_connection,
5execute_ssh_command, etc.
6"""
7
8import sys
9import re
Michael Walsh2575cd62017-11-01 17:03:45 -050010import socket
Michael Walshbd1bb602017-06-30 17:01:25 -050011import paramiko
12import exceptions
13
14import gen_print as gp
Michael Walsha48eb1a2018-08-07 14:48:13 -050015import func_timer as ft
16func_timer = ft.func_timer_class()
Michael Walshbd1bb602017-06-30 17:01:25 -050017
18from robot.libraries.BuiltIn import BuiltIn
19from SSHLibrary import SSHLibrary
20sshlib = SSHLibrary()
21
22
Michael Walshbd1bb602017-06-30 17:01:25 -050023def sprint_connection(connection,
24 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050025 r"""
26 sprint data from the connection object to a string and return it.
27
28 connection A connection object which is created by
29 the SSHlibrary open_connection() function.
30 indent The number of characters to indent the
31 output.
32 """
33
34 buffer = gp.sindent("", indent)
35 buffer += "connection:\n"
36 indent += 2
37 buffer += gp.sprint_varx("index", connection.index, 0, indent)
38 buffer += gp.sprint_varx("host", connection.host, 0, indent)
39 buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
40 buffer += gp.sprint_varx("port", connection.port, 0, indent)
41 buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
42 buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
43 buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
44 buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
45 buffer += gp.sprint_varx("width", connection.width, 0, indent)
46 buffer += gp.sprint_varx("height", connection.height, 0, indent)
47 buffer += gp.sprint_varx("path_separator", connection.path_separator, 0,
48 indent)
49 buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
50
51 return buffer
52
Michael Walshbd1bb602017-06-30 17:01:25 -050053
Michael Walshbd1bb602017-06-30 17:01:25 -050054def sprint_connections(connections=None,
55 indent=0):
Michael Walshbd1bb602017-06-30 17:01:25 -050056 r"""
57 sprint data from the connections list to a string and return it.
58
59 connections A list of connection objects which are
60 created by the SSHlibrary open_connection
61 function. If this value is null, this
62 function will populate with a call to the
63 SSHlibrary get_connections() function.
64 indent The number of characters to indent the
65 output.
66 """
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"""
80 Find connection that matches the given connection arguments and return
81 connection object. Return False if no matching connection is found.
82
83 Description of argument(s):
84 open_connection_args A dictionary of arg names and values which
85 are legal to pass to the SSHLibrary
86 open_connection function as parms/args.
87 For a match to occur, the value for each
88 item in open_connection_args must match
89 the corresponding value in the connection
90 being examined.
91 """
92
93 global sshlib
94
95 for connection in sshlib.get_connections():
96 # Create connection_dict from connection object.
97 connection_dict = dict((key, str(value)) for key, value in
98 connection._config.iteritems())
99 if dict(connection_dict, **open_connection_args) == connection_dict:
100 return connection
101
102 return False
103
Michael Walshbd1bb602017-06-30 17:01:25 -0500104
Michael Walshbd1bb602017-06-30 17:01:25 -0500105def login_ssh(login_args={},
106 max_login_attempts=5):
Michael Walshbd1bb602017-06-30 17:01:25 -0500107 r"""
108 Login on the latest open SSH connection. Retry on failure up to
109 max_login_attempts.
110
111 The caller is responsible for making sure there is an open SSH connection.
112
113 Description of argument(s):
114 login_args A dictionary containing the key/value
115 pairs which are acceptable to the
116 SSHLibrary login function as parms/args.
117 At a minimum, this should contain a
118 'username' and a 'password' entry.
119 max_login_attempts The max number of times to try logging in
120 (in the event of login failures).
121 """
122
Michael Walsha03a3312017-12-01 16:47:44 -0600123 gp.lprint_executing()
124
Michael Walshbd1bb602017-06-30 17:01:25 -0500125 global sshlib
126
127 # Get connection data for debug output.
128 connection = sshlib.get_connection()
Michael Walsha03a3312017-12-01 16:47:44 -0600129 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500130 for login_attempt_num in range(1, max_login_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600131 gp.lprint_timen("Logging in to " + connection.host + ".")
132 gp.lprint_var(login_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500133 try:
134 out_buf = sshlib.login(**login_args)
135 except Exception as login_exception:
136 # Login will sometimes fail if the connection is new.
137 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600138 gp.lprint_var(except_type)
139 gp.lprint_varx("except_value", str(except_value))
Michael Walshbd1bb602017-06-30 17:01:25 -0500140 if except_type is paramiko.ssh_exception.SSHException and\
141 re.match(r"No existing session", str(except_value)):
142 continue
143 else:
144 # We don't tolerate any other error so break from loop and
145 # re-raise exception.
146 break
147 # If we get to this point, the login has worked and we can return.
Michael Walsha03a3312017-12-01 16:47:44 -0600148 gp.lpvar(out_buf)
Michael Walshbd1bb602017-06-30 17:01:25 -0500149 return
150
151 # If we get to this point, the login has failed on all attempts so the
152 # exception will be raised again.
153 raise(login_exception)
154
Michael Walshbd1bb602017-06-30 17:01:25 -0500155
Michael Walshbd1bb602017-06-30 17:01:25 -0500156def execute_ssh_command(cmd_buf,
157 open_connection_args={},
158 login_args={},
159 print_out=0,
160 print_err=0,
161 ignore_err=1,
162 fork=0,
163 quiet=None,
Michael Walsha48eb1a2018-08-07 14:48:13 -0500164 test_mode=None,
165 time_out=None):
Michael Walshbd1bb602017-06-30 17:01:25 -0500166 r"""
167 Run the given command in an SSH session and return the stdout, stderr and
168 the return code.
169
170 If there is no open SSH connection, this function will connect and login.
171 Likewise, if the caller has not yet logged in to the connection, this
172 function will do the login.
173
Michael Walsh8243ac92018-05-02 13:47:07 -0500174 NOTE: There is special handling when open_connection_args['alias'] equals
175 "device_connection".
176 - A write, rather than an execute_command, is done.
177 - Only stdout is returned (no stderr or rc).
178 - print_err, ignore_err and fork are not supported.
179
Michael Walshbd1bb602017-06-30 17:01:25 -0500180 Description of arguments:
181 cmd_buf The command string to be run in an SSH
182 session.
183 open_connection_args A dictionary of arg names and values which
184 are legal to pass to the SSHLibrary
185 open_connection function as parms/args.
186 At a minimum, this should contain a 'host'
187 entry.
188 login_args A dictionary containing the key/value
189 pairs which are acceptable to the
190 SSHLibrary login function as parms/args.
191 At a minimum, this should contain a
192 'username' and a 'password' entry.
193 print_out If this is set, this function will print
194 the stdout/stderr generated by the shell
195 command.
196 print_err If show_err is set, this function will
197 print a standardized error report if the
198 shell command returns non-zero.
199 ignore_err Indicates that errors encountered on the
200 sshlib.execute_command are to be ignored.
201 fork Indicates that sshlib.start is to be used
202 rather than sshlib.execute_command.
203 quiet Indicates whether this function should run
204 the pissuing() function which prints an
205 "Issuing: <cmd string>" to stdout. This
206 defaults to the global quiet value.
207 test_mode If test_mode is set, this function will
208 not actually run the command. This
209 defaults to the global test_mode value.
Michael Walsha48eb1a2018-08-07 14:48:13 -0500210 time_out The amount of time to allow for the
211 execution of cmd_buf. A value of None
212 means that there is no limit to how long
213 the command may take.
Michael Walshbd1bb602017-06-30 17:01:25 -0500214 """
215
Michael Walsha03a3312017-12-01 16:47:44 -0600216 gp.lprint_executing()
Michael Walshbd1bb602017-06-30 17:01:25 -0500217
218 # Obtain default values.
219 quiet = int(gp.get_var_value(quiet, 0))
220 test_mode = int(gp.get_var_value(test_mode, 0))
221
222 if not quiet:
223 gp.pissuing(cmd_buf, test_mode)
Michael Walsha03a3312017-12-01 16:47:44 -0600224 gp.lpissuing(cmd_buf, test_mode)
Michael Walshbd1bb602017-06-30 17:01:25 -0500225
226 if test_mode:
227 return "", "", 0
228
229 global sshlib
230
Michael Walsh019817d2018-07-24 16:00:06 -0500231 max_exec_cmd_attempts = 2
Michael Walshbd1bb602017-06-30 17:01:25 -0500232 # Look for existing SSH connection.
233 # Prepare a search connection dictionary.
234 search_connection_args = open_connection_args.copy()
235 # Remove keys that don't work well for searches.
236 search_connection_args.pop("timeout", None)
237 connection = find_connection(search_connection_args)
238 if connection:
Michael Walsha03a3312017-12-01 16:47:44 -0600239 gp.lprint_timen("Found the following existing connection:")
240 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500241 if connection.alias == "":
242 index_or_alias = connection.index
243 else:
244 index_or_alias = connection.alias
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500245 gp.lprint_timen("Switching to existing connection: \""
246 + str(index_or_alias) + "\".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500247 sshlib.switch_connection(index_or_alias)
248 else:
Michael Walsha03a3312017-12-01 16:47:44 -0600249 gp.lprint_timen("Connecting to " + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500250 cix = sshlib.open_connection(**open_connection_args)
Michael Walsh019817d2018-07-24 16:00:06 -0500251 try:
252 login_ssh(login_args)
253 except Exception as login_exception:
254 except_type, except_value, except_traceback = sys.exc_info()
255 rc = 1
256 stderr = str(except_value)
257 stdout = ""
258 max_exec_cmd_attempts = 0
Michael Walshbd1bb602017-06-30 17:01:25 -0500259
Michael Walshbd1bb602017-06-30 17:01:25 -0500260 for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600261 gp.lprint_var(exec_cmd_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500262 try:
263 if fork:
264 sshlib.start_command(cmd_buf)
265 else:
Michael Walsh8243ac92018-05-02 13:47:07 -0500266 if open_connection_args['alias'] == "device_connection":
267 stdout = sshlib.write(cmd_buf)
268 stderr = ""
269 rc = 0
270 else:
271 stdout, stderr, rc = \
Michael Walsha48eb1a2018-08-07 14:48:13 -0500272 func_timer.run(sshlib.execute_command,
273 cmd_buf,
274 return_stdout=True,
275 return_stderr=True,
276 return_rc=True,
277 time_out=time_out)
Michael Walshbd1bb602017-06-30 17:01:25 -0500278 except Exception as execute_exception:
279 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600280 gp.lprint_var(except_type)
281 gp.lprint_varx("except_value", str(except_value))
Michael Walshbd1bb602017-06-30 17:01:25 -0500282
283 if except_type is exceptions.AssertionError and\
284 re.match(r"Connection not open", str(except_value)):
285 login_ssh(login_args)
286 # Now we must continue to next loop iteration to retry the
287 # execute_command.
288 continue
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500289 if (except_type is paramiko.ssh_exception.SSHException
290 and re.match(r"SSH session not active", str(except_value))) or\
291 (except_type is socket.error
292 and re.match(r"\[Errno 104\] Connection reset by peer",
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()
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500302 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
308 # We do not handle any other RuntimeErrors so we will raise the
309 # exception again.
Michael Walshe77585a2017-12-14 11:02:28 -0600310 sshlib.close_all_connections()
Michael Walshbd1bb602017-06-30 17:01:25 -0500311 raise(execute_exception)
312
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:
320 gp.print_var(rc, 1)
321 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:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500329 message = gp.sprint_error("The prior SSH"
330 + " command returned a non-zero return"
331 + " code:\n" + gp.sprint_var(rc, 1) + stderr
332 + "\n")
Michael Walshbd1bb602017-06-30 17:01:25 -0500333 BuiltIn().should_be_equal(rc, 0, message)
334
Michael Walsh8243ac92018-05-02 13:47:07 -0500335 if open_connection_args['alias'] == "device_connection":
336 return stdout
Michael Walshbd1bb602017-06-30 17:01:25 -0500337 return stdout, stderr, rc