1#!/usr/bin/env python3
2
3r"""
4This module provides many valuable ssh functions such as sprint_connection, execute_ssh_command, etc.
5"""
6
7import sys
8import traceback
9import re
10import socket
11import paramiko
12try:
13    import exceptions
14except ImportError:
15    import builtins as exceptions
16
17import gen_print as gp
18import func_timer as ft
19func_timer = ft.func_timer_class()
20
21from robot.libraries.BuiltIn import BuiltIn
22from SSHLibrary import SSHLibrary
23sshlib = SSHLibrary()
24
25
26def sprint_connection(connection,
27                      indent=0):
28    r"""
29    sprint data from the connection object to a string and return it.
30
31    connection                      A connection object which is created by the SSHlibrary open_connection()
32                                    function.
33    indent                          The number of characters to indent the output.
34    """
35
36    buffer = gp.sindent("", indent)
37    buffer += "connection:\n"
38    indent += 2
39    buffer += gp.sprint_varx("index", connection.index, 0, indent)
40    buffer += gp.sprint_varx("host", connection.host, 0, indent)
41    buffer += gp.sprint_varx("alias", connection.alias, 0, indent)
42    buffer += gp.sprint_varx("port", connection.port, 0, indent)
43    buffer += gp.sprint_varx("timeout", connection.timeout, 0, indent)
44    buffer += gp.sprint_varx("newline", connection.newline, 0, indent)
45    buffer += gp.sprint_varx("prompt", connection.prompt, 0, indent)
46    buffer += gp.sprint_varx("term_type", connection.term_type, 0, indent)
47    buffer += gp.sprint_varx("width", connection.width, 0, indent)
48    buffer += gp.sprint_varx("height", connection.height, 0, indent)
49    buffer += gp.sprint_varx("path_separator", connection.path_separator, 0,
50                             indent)
51    buffer += gp.sprint_varx("encoding", connection.encoding, 0, indent)
52
53    return buffer
54
55
56def sprint_connections(connections=None,
57                       indent=0):
58    r"""
59    sprint data from the connections list to a string and return it.
60
61    connections                     A list of connection objects which are created by the SSHlibrary
62                                    open_connection function.  If this value is null, this function will
63                                    populate with a call to the SSHlibrary get_connections() function.
64    indent                          The number of characters to indent the output.
65    """
66
67    if connections is None:
68        connections = sshlib.get_connections()
69
70    buffer = ""
71    for connection in connections:
72        buffer += sprint_connection(connection, indent)
73
74    return buffer
75
76
77def find_connection(open_connection_args={}):
78    r"""
79    Find connection that matches the given connection arguments and return connection object.  Return False
80    if no matching connection is found.
81
82    Description of argument(s):
83    open_connection_args            A dictionary of arg names and values which are legal to pass to the
84                                    SSHLibrary open_connection function as parms/args.  For a match to occur,
85                                    the value for each item in open_connection_args must match the
86                                    corresponding value in the connection being examined.
87    """
88
89    global sshlib
90
91    for connection in sshlib.get_connections():
92        # Create connection_dict from connection object.
93        connection_dict = dict((key, str(value)) for key, value in
94                               connection._config.items())
95        if dict(connection_dict, **open_connection_args) == connection_dict:
96            return connection
97
98    return False
99
100
101def login_ssh(login_args={},
102              max_login_attempts=5):
103    r"""
104    Login on the latest open SSH connection.  Retry on failure up to max_login_attempts.
105
106    The caller is responsible for making sure there is an open SSH connection.
107
108    Description of argument(s):
109    login_args                      A dictionary containing the key/value pairs which are acceptable to the
110                                    SSHLibrary login function as parms/args.  At a minimum, this should
111                                    contain a 'username' and a 'password' entry.
112    max_login_attempts              The max number of times to try logging in (in the event of login
113                                    failures).
114    """
115
116    gp.lprint_executing()
117
118    global sshlib
119
120    # Get connection data for debug output.
121    connection = sshlib.get_connection()
122    gp.lprintn(sprint_connection(connection))
123    for login_attempt_num in range(1, max_login_attempts + 1):
124        gp.lprint_timen("Logging in to " + connection.host + ".")
125        gp.lprint_var(login_attempt_num)
126        try:
127            out_buf = sshlib.login(**login_args)
128        except Exception:
129            # Login will sometimes fail if the connection is new.
130            except_type, except_value, except_traceback = sys.exc_info()
131            gp.lprint_var(except_type)
132            gp.lprint_varx("except_value", str(except_value))
133            if except_type is paramiko.ssh_exception.SSHException and\
134                    re.match(r"No existing session", str(except_value)):
135                continue
136            else:
137                # We don't tolerate any other error so break from loop and re-raise exception.
138                break
139        # If we get to this point, the login has worked and we can return.
140        gp.lprint_var(out_buf)
141        return
142
143    # If we get to this point, the login has failed on all attempts so the exception will be raised again.
144    raise(except_value)
145
146
147def execute_ssh_command(cmd_buf,
148                        open_connection_args={},
149                        login_args={},
150                        print_out=0,
151                        print_err=0,
152                        ignore_err=1,
153                        fork=0,
154                        quiet=None,
155                        test_mode=None,
156                        time_out=None):
157    r"""
158    Run the given command in an SSH session and return the stdout, stderr and the return code.
159
160    If there is no open SSH connection, this function will connect and login.  Likewise, if the caller has
161    not yet logged in to the connection, this function will do the login.
162
163    NOTE: There is special handling when open_connection_args['alias'] equals "device_connection".
164    - A write, rather than an execute_command, is done.
165    - Only stdout is returned (no stderr or rc).
166    - print_err, ignore_err and fork are not supported.
167
168    Description of arguments:
169    cmd_buf                         The command string to be run in an SSH session.
170    open_connection_args            A dictionary of arg names and values which are legal to pass to the
171                                    SSHLibrary open_connection function as parms/args.  At a minimum, this
172                                    should contain a 'host' entry.
173    login_args                      A dictionary containing the key/value pairs which are acceptable to the
174                                    SSHLibrary login function as parms/args.  At a minimum, this should
175                                    contain a 'username' and a 'password' entry.
176    print_out                       If this is set, this function will print the stdout/stderr generated by
177                                    the shell command.
178    print_err                       If show_err is set, this function will print a standardized error report
179                                    if the shell command returns non-zero.
180    ignore_err                      Indicates that errors encountered on the sshlib.execute_command are to be
181                                    ignored.
182    fork                            Indicates that sshlib.start is to be used rather than
183                                    sshlib.execute_command.
184    quiet                           Indicates whether this function should run the pissuing() function which
185                                    prints an "Issuing: <cmd string>" to stdout.  This defaults to the global
186                                    quiet value.
187    test_mode                       If test_mode is set, this function will not actually run the command.
188                                    This defaults to the global test_mode value.
189    time_out                        The amount of time to allow for the execution of cmd_buf.  A value of
190                                    None means that there is no limit to how long the command may take.
191    """
192
193    gp.lprint_executing()
194
195    # Obtain default values.
196    quiet = int(gp.get_var_value(quiet, 0))
197    test_mode = int(gp.get_var_value(test_mode, 0))
198
199    if not quiet:
200        gp.pissuing(cmd_buf, test_mode)
201    gp.lpissuing(cmd_buf, test_mode)
202
203    if test_mode:
204        return "", "", 0
205
206    global sshlib
207
208    max_exec_cmd_attempts = 2
209    # Look for existing SSH connection.
210    # Prepare a search connection dictionary.
211    search_connection_args = open_connection_args.copy()
212    # Remove keys that don't work well for searches.
213    search_connection_args.pop("timeout", None)
214    connection = find_connection(search_connection_args)
215    if connection:
216        gp.lprint_timen("Found the following existing connection:")
217        gp.lprintn(sprint_connection(connection))
218        if connection.alias == "":
219            index_or_alias = connection.index
220        else:
221            index_or_alias = connection.alias
222        gp.lprint_timen("Switching to existing connection: \""
223                        + str(index_or_alias) + "\".")
224        sshlib.switch_connection(index_or_alias)
225    else:
226        gp.lprint_timen("Connecting to " + open_connection_args['host'] + ".")
227        cix = sshlib.open_connection(**open_connection_args)
228        try:
229            login_ssh(login_args)
230        except Exception:
231            except_type, except_value, except_traceback = sys.exc_info()
232            rc = 1
233            stderr = str(except_value)
234            stdout = ""
235            max_exec_cmd_attempts = 0
236
237    for exec_cmd_attempt_num in range(1, max_exec_cmd_attempts + 1):
238        gp.lprint_var(exec_cmd_attempt_num)
239        try:
240            if fork:
241                sshlib.start_command(cmd_buf)
242            else:
243                if open_connection_args['alias'] == "device_connection":
244                    stdout = sshlib.write(cmd_buf)
245                    stderr = ""
246                    rc = 0
247                else:
248                    stdout, stderr, rc = \
249                        func_timer.run(sshlib.execute_command,
250                                       cmd_buf,
251                                       return_stdout=True,
252                                       return_stderr=True,
253                                       return_rc=True,
254                                       time_out=time_out)
255        except Exception:
256            except_type, except_value, except_traceback = sys.exc_info()
257            gp.lprint_var(except_type)
258            gp.lprint_varx("except_value", str(except_value))
259            # This may be our last time through the retry loop, so setting
260            # return variables.
261            rc = 1
262            stderr = str(except_value)
263            stdout = ""
264
265            if except_type is exceptions.AssertionError and\
266               re.match(r"Connection not open", str(except_value)):
267                try:
268                    login_ssh(login_args)
269                    # Now we must continue to next loop iteration to retry the
270                    # execute_command.
271                    continue
272                except Exception:
273                    except_type, except_value, except_traceback =\
274                        sys.exc_info()
275                    rc = 1
276                    stderr = str(except_value)
277                    stdout = ""
278                    break
279
280            if (except_type is paramiko.ssh_exception.SSHException
281                and re.match(r"SSH session not active", str(except_value))) or\
282               ((except_type is socket.error
283                 or except_type is ConnectionResetError)
284                and re.match(r"\[Errno 104\] Connection reset by peer",
285                             str(except_value))) or\
286               (except_type is paramiko.ssh_exception.SSHException
287                and re.match(r"Timeout opening channel\.",
288                             str(except_value))):
289                # Close and re-open a connection.
290                # Note: close_connection() doesn't appear to get rid of the
291                # connection.  It merely closes it.  Since there is a concern
292                # about over-consumption of resources, we use
293                # close_all_connections() which also gets rid of all
294                # connections.
295                gp.lprint_timen("Closing all connections.")
296                sshlib.close_all_connections()
297                gp.lprint_timen("Connecting to "
298                                + open_connection_args['host'] + ".")
299                cix = sshlib.open_connection(**open_connection_args)
300                login_ssh(login_args)
301                continue
302
303            # We do not handle any other RuntimeErrors so we will raise the exception again.
304            sshlib.close_all_connections()
305            gp.lprintn(traceback.format_exc())
306            raise(except_value)
307
308        # If we get to this point, the command was executed.
309        break
310
311    if fork:
312        return
313
314    if rc != 0 and print_err:
315        gp.print_var(rc, gp.hexa())
316        if not print_out:
317            gp.print_var(stderr)
318            gp.print_var(stdout)
319
320    if print_out:
321        gp.printn(stderr + stdout)
322
323    if not ignore_err:
324        message = gp.sprint_error("The prior SSH"
325                                  + " command returned a non-zero return"
326                                  + " code:\n"
327                                  + gp.sprint_var(rc, gp.hexa()) + stderr
328                                  + "\n")
329        BuiltIn().should_be_equal(rc, 0, message)
330
331    if open_connection_args['alias'] == "device_connection":
332        return stdout
333    return stdout, stderr, rc
334