blob: 1ccf7031e8f8b27f52473a0f953bf5e2b915a801 [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
15
16from robot.libraries.BuiltIn import BuiltIn
17from SSHLibrary import SSHLibrary
18sshlib = SSHLibrary()
19
20
Michael Walshbd1bb602017-06-30 17:01:25 -050021def sprint_connection(connection,
22 indent=0):
23
24 r"""
25 sprint data from the connection object to a string and return it.
26
27 connection A connection object which is created by
28 the SSHlibrary open_connection() function.
29 indent The number of characters to indent the
30 output.
31 """
32
33 buffer = gp.sindent("", indent)
34 buffer += "connection:\n"
35 indent += 2
36 buffer += gp.sprint_varx("index", connection.index, 0, indent)
37 buffer += gp.sprint_varx("host", connection.host, 0, indent)
38 buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
39 buffer += gp.sprint_varx("port", connection.port, 0, indent)
40 buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
41 buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
42 buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
43 buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
44 buffer += gp.sprint_varx("width", connection.width, 0, indent)
45 buffer += gp.sprint_varx("height", connection.height, 0, indent)
46 buffer += gp.sprint_varx("path_separator", connection.path_separator, 0,
47 indent)
48 buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
49
50 return buffer
51
Michael Walshbd1bb602017-06-30 17:01:25 -050052
Michael Walshbd1bb602017-06-30 17:01:25 -050053def sprint_connections(connections=None,
54 indent=0):
55
56 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={}):
79
80 r"""
81 Find connection that matches the given connection arguments and return
82 connection object. Return False if no matching connection is found.
83
84 Description of argument(s):
85 open_connection_args A dictionary of arg names and values which
86 are legal to pass to the SSHLibrary
87 open_connection function as parms/args.
88 For a match to occur, the value for each
89 item in open_connection_args must match
90 the corresponding value in the connection
91 being examined.
92 """
93
94 global sshlib
95
96 for connection in sshlib.get_connections():
97 # Create connection_dict from connection object.
98 connection_dict = dict((key, str(value)) for key, value in
99 connection._config.iteritems())
100 if dict(connection_dict, **open_connection_args) == connection_dict:
101 return connection
102
103 return False
104
Michael Walshbd1bb602017-06-30 17:01:25 -0500105
Michael Walshbd1bb602017-06-30 17:01:25 -0500106def login_ssh(login_args={},
107 max_login_attempts=5):
108
109 r"""
110 Login on the latest open SSH connection. Retry on failure up to
111 max_login_attempts.
112
113 The caller is responsible for making sure there is an open SSH connection.
114
115 Description of argument(s):
116 login_args A dictionary containing the key/value
117 pairs which are acceptable to the
118 SSHLibrary login function as parms/args.
119 At a minimum, this should contain a
120 'username' and a 'password' entry.
121 max_login_attempts The max number of times to try logging in
122 (in the event of login failures).
123 """
124
Michael Walsha03a3312017-12-01 16:47:44 -0600125 gp.lprint_executing()
126
Michael Walshbd1bb602017-06-30 17:01:25 -0500127 global sshlib
128
129 # Get connection data for debug output.
130 connection = sshlib.get_connection()
Michael Walsha03a3312017-12-01 16:47:44 -0600131 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500132 for login_attempt_num in range(1, max_login_attempts + 1):
Michael Walsha03a3312017-12-01 16:47:44 -0600133 gp.lprint_timen("Logging in to " + connection.host + ".")
134 gp.lprint_var(login_attempt_num)
Michael Walshbd1bb602017-06-30 17:01:25 -0500135 try:
136 out_buf = sshlib.login(**login_args)
137 except Exception as login_exception:
138 # Login will sometimes fail if the connection is new.
139 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600140 gp.lprint_var(except_type)
141 gp.lprint_varx("except_value", str(except_value))
Michael Walshbd1bb602017-06-30 17:01:25 -0500142 if except_type is paramiko.ssh_exception.SSHException and\
143 re.match(r"No existing session", str(except_value)):
144 continue
145 else:
146 # We don't tolerate any other error so break from loop and
147 # re-raise exception.
148 break
149 # If we get to this point, the login has worked and we can return.
Michael Walsha03a3312017-12-01 16:47:44 -0600150 gp.lpvar(out_buf)
Michael Walshbd1bb602017-06-30 17:01:25 -0500151 return
152
153 # If we get to this point, the login has failed on all attempts so the
154 # exception will be raised again.
155 raise(login_exception)
156
Michael Walshbd1bb602017-06-30 17:01:25 -0500157
Michael Walshbd1bb602017-06-30 17:01:25 -0500158def execute_ssh_command(cmd_buf,
159 open_connection_args={},
160 login_args={},
161 print_out=0,
162 print_err=0,
163 ignore_err=1,
164 fork=0,
165 quiet=None,
166 test_mode=None):
167
168 r"""
169 Run the given command in an SSH session and return the stdout, stderr and
170 the return code.
171
172 If there is no open SSH connection, this function will connect and login.
173 Likewise, if the caller has not yet logged in to the connection, this
174 function will do the login.
175
176 Description of arguments:
177 cmd_buf The command string to be run in an SSH
178 session.
179 open_connection_args A dictionary of arg names and values which
180 are legal to pass to the SSHLibrary
181 open_connection function as parms/args.
182 At a minimum, this should contain a 'host'
183 entry.
184 login_args A dictionary containing the key/value
185 pairs which are acceptable to the
186 SSHLibrary login function as parms/args.
187 At a minimum, this should contain a
188 'username' and a 'password' entry.
189 print_out If this is set, this function will print
190 the stdout/stderr generated by the shell
191 command.
192 print_err If show_err is set, this function will
193 print a standardized error report if the
194 shell command returns non-zero.
195 ignore_err Indicates that errors encountered on the
196 sshlib.execute_command are to be ignored.
197 fork Indicates that sshlib.start is to be used
198 rather than sshlib.execute_command.
199 quiet Indicates whether this function should run
200 the pissuing() function which prints an
201 "Issuing: <cmd string>" to stdout. This
202 defaults to the global quiet value.
203 test_mode If test_mode is set, this function will
204 not actually run the command. This
205 defaults to the global test_mode value.
206 """
207
Michael Walsha03a3312017-12-01 16:47:44 -0600208 gp.lprint_executing()
Michael Walshbd1bb602017-06-30 17:01:25 -0500209
210 # Obtain default values.
211 quiet = int(gp.get_var_value(quiet, 0))
212 test_mode = int(gp.get_var_value(test_mode, 0))
213
214 if not quiet:
215 gp.pissuing(cmd_buf, test_mode)
Michael Walsha03a3312017-12-01 16:47:44 -0600216 gp.lpissuing(cmd_buf, test_mode)
Michael Walshbd1bb602017-06-30 17:01:25 -0500217
218 if test_mode:
219 return "", "", 0
220
221 global sshlib
222
223 # Look for existing SSH connection.
224 # Prepare a search connection dictionary.
225 search_connection_args = open_connection_args.copy()
226 # Remove keys that don't work well for searches.
227 search_connection_args.pop("timeout", None)
228 connection = find_connection(search_connection_args)
229 if connection:
Michael Walsha03a3312017-12-01 16:47:44 -0600230 gp.lprint_timen("Found the following existing connection:")
231 gp.lprintn(sprint_connection(connection))
Michael Walshbd1bb602017-06-30 17:01:25 -0500232 if connection.alias == "":
233 index_or_alias = connection.index
234 else:
235 index_or_alias = connection.alias
Michael Walsha03a3312017-12-01 16:47:44 -0600236 gp.lprint_timen("Switching to existing connection: \"" +
Michael Walshbd1bb602017-06-30 17:01:25 -0500237 str(index_or_alias) + "\".")
238 sshlib.switch_connection(index_or_alias)
239 else:
Michael Walsha03a3312017-12-01 16:47:44 -0600240 gp.lprint_timen("Connecting to " + open_connection_args['host'] + ".")
Michael Walshbd1bb602017-06-30 17:01:25 -0500241 cix = sshlib.open_connection(**open_connection_args)
242 login_ssh(login_args)
243
244 max_exec_cmd_attempts = 2
245 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:
251 stdout, stderr, rc = sshlib.execute_command(cmd_buf,
252 return_stdout=True,
253 return_stderr=True,
254 return_rc=True)
255 except Exception as execute_exception:
256 except_type, except_value, except_traceback = sys.exc_info()
Michael Walsha03a3312017-12-01 16:47:44 -0600257 gp.lprint_var(except_type)
258 gp.lprint_varx("except_value", str(except_value))
Michael Walshbd1bb602017-06-30 17:01:25 -0500259
260 if except_type is exceptions.AssertionError and\
261 re.match(r"Connection not open", str(except_value)):
262 login_ssh(login_args)
263 # Now we must continue to next loop iteration to retry the
264 # execute_command.
265 continue
Michael Walsh2575cd62017-11-01 17:03:45 -0500266 if (except_type is paramiko.ssh_exception.SSHException and
267 re.match(r"SSH session not active", str(except_value))) or\
268 (except_type is socket.error and
269 re.match(r"\[Errno 104\] Connection reset by peer",
270 str(except_value))):
Michael Walshbd1bb602017-06-30 17:01:25 -0500271 # Close and re-open a connection.
Michael Walsh2575cd62017-11-01 17:03:45 -0500272 # Note: close_connection() doesn't appear to get rid of the
273 # connection. It merely closes it. Since there is a concern
274 # about over-consumption of resources, we use
275 # close_all_connections() which also gets rid of all
276 # connections.
Michael Walsha03a3312017-12-01 16:47:44 -0600277 gp.lprint_timen("Closing all connections.")
Michael Walsh2575cd62017-11-01 17:03:45 -0500278 sshlib.close_all_connections()
Michael Walsha03a3312017-12-01 16:47:44 -0600279 gp.lprint_timen("Connecting to " +
Michael Walshbd1bb602017-06-30 17:01:25 -0500280 open_connection_args['host'] + ".")
281 cix = sshlib.open_connection(**open_connection_args)
282 login_ssh(login_args)
283 continue
284
285 # We do not handle any other RuntimeErrors so we will raise the
286 # exception again.
Michael Walshe77585a2017-12-14 11:02:28 -0600287 sshlib.close_all_connections()
Michael Walshbd1bb602017-06-30 17:01:25 -0500288 raise(execute_exception)
289
290 # If we get to this point, the command was executed.
291 break
292
293 if fork:
294 return
295
296 if rc != 0 and print_err:
297 gp.print_var(rc, 1)
298 if not print_out:
299 gp.print_var(stderr)
300 gp.print_var(stdout)
301
302 if print_out:
303 gp.printn(stderr + stdout)
304
305 if not ignore_err:
306 message = gp.sprint_error("The prior SSH" +
307 " command returned a non-zero return" +
308 " code:\n" + gp.sprint_var(rc, 1) + stderr +
309 "\n")
310 BuiltIn().should_be_equal(rc, 0, message)
311
312 return stdout, stderr, rc