X-Git-Url: https://gerrit.fd.io/r/gitweb?p=csit.git;a=blobdiff_plain;f=resources%2Flibraries%2Fpython%2Fssh.py;h=67193c11e2ca8e9082089d2535d8fdde4f48bd38;hp=385619ce42666476ed80390689eb035cde6a013f;hb=2e6b88e7c414e31336fd6644143b257e94b89624;hpb=da23519d72dc9415b112f7bab1fd3617750fa79e diff --git a/resources/libraries/python/ssh.py b/resources/libraries/python/ssh.py index 385619ce42..67193c11e2 100644 --- a/resources/libraries/python/ssh.py +++ b/resources/libraries/python/ssh.py @@ -10,12 +10,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import StringIO +from time import time + +import socket import paramiko +from paramiko import RSAKey from scp import SCPClient -from time import time -from robot.api import logger from interruptingcow import timeout -from robot.utils.asserts import assert_equal, assert_not_equal +from robot.api import logger +from robot.utils.asserts import assert_equal __all__ = ["exec_cmd", "exec_cmd_no_error"] @@ -28,11 +33,10 @@ class SSH(object): __existing_connections = {} def __init__(self): - self._ssh = paramiko.SSHClient() - self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self._hostname = None + self._ssh = None - def _node_hash(self, node): + @staticmethod + def _node_hash(node): return hash(frozenset([node['host'], node['port']])) def connect(self, node): @@ -40,38 +44,71 @@ class SSH(object): If there already is a connection to the node, this method reuses it. """ - self._hostname = node['host'] node_hash = self._node_hash(node) - if node_hash in self.__existing_connections: - self._ssh = self.__existing_connections[node_hash] + if node_hash in SSH.__existing_connections: + self._ssh = SSH.__existing_connections[node_hash] + logger.debug('reusing ssh: {0}'.format(self._ssh)) else: start = time() + pkey = None + if 'priv_key' in node: + pkey = RSAKey.from_private_key( + StringIO.StringIO(node['priv_key'])) + + self._ssh = paramiko.SSHClient() + self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self._ssh.connect(node['host'], username=node['username'], - password=node['password']) - self.__existing_connections[node_hash] = self._ssh + password=node.get('password'), pkey=pkey, + port=node['port']) + + SSH.__existing_connections[node_hash] = self._ssh + logger.trace('connect took {} seconds'.format(time() - start)) + logger.debug('new ssh: {0}'.format(self._ssh)) + + logger.debug('Connect peer: {0}'. + format(self._ssh.get_transport().getpeername())) + logger.debug('Connections: {0}'.format(str(SSH.__existing_connections))) + + def disconnect(self, node): + """Close SSH connection to the node. + + :param node: The node to disconnect from. + :type node: dict + """ + node_hash = self._node_hash(node) + if node_hash in SSH.__existing_connections: + ssh = SSH.__existing_connections.pop(node_hash) + ssh.close() def exec_command(self, cmd, timeout=10): """Execute SSH command on a new channel on the connected Node. Returns (return_code, stdout, stderr). """ - logger.trace('exec_command on {0}: {1}'.format(self._hostname, cmd)) + logger.trace('exec_command on {0}: {1}' + .format(self._ssh.get_transport().getpeername(), cmd)) start = time() chan = self._ssh.get_transport().open_session() if timeout is not None: chan.settimeout(int(timeout)) chan.exec_command(cmd) end = time() - logger.trace('exec_command "{0}" on {1} took {2} seconds'.format( - cmd, self._hostname, end-start)) + logger.trace('exec_command on {0} took {1} seconds'.format( + self._ssh.get_transport().getpeername(), end-start)) stdout = "" - while True: - buf = chan.recv(self.__MAX_RECV_BUF) - stdout += buf - if not buf: - break + try: + while True: + buf = chan.recv(self.__MAX_RECV_BUF) + stdout += buf + if not buf: + break + except socket.timeout: + logger.error('Caught timeout exception, current contents ' + 'of buffer: {0}'.format(stdout)) + raise stderr = "" while True: @@ -83,25 +120,28 @@ class SSH(object): return_code = chan.recv_exit_status() logger.trace('chan_recv/_stderr took {} seconds'.format(time()-end)) - return (return_code, stdout, stderr) + logger.trace('return RC {}'.format(return_code)) + logger.trace('return STDOUT {}'.format(stdout)) + logger.trace('return STDERR {}'.format(stderr)) + return return_code, stdout, stderr def exec_command_sudo(self, cmd, cmd_input=None, timeout=10): """Execute SSH command with sudo on a new channel on the connected Node. - :param cmd: Command to be executed. - :param cmd_input: Input redirected to the command. - :param timeout: Timeout. - :return: return_code, stdout, stderr + :param cmd: Command to be executed. + :param cmd_input: Input redirected to the command. + :param timeout: Timeout. + :return: return_code, stdout, stderr - :Example: + :Example: - >>> from ssh import SSH - >>> ssh = SSH() - >>> ssh.connect(node) - >>> #Execute command without input (sudo -S cmd) - >>> ssh.exex_command_sudo("ifconfig eth0 down") - >>> #Execute command with input (sudo -S cmd <<< "input") - >>> ssh.exex_command_sudo("vpp_api_test", "dump_interface_table") + >>> from ssh import SSH + >>> ssh = SSH() + >>> ssh.connect(node) + >>> # Execute command without input (sudo -S cmd) + >>> ssh.exec_command_sudo("ifconfig eth0 down") + >>> # Execute command with input (sudo -S cmd <<< "input") + >>> ssh.exec_command_sudo("vpp_api_test", "dump_interface_table") """ if cmd_input is None: command = 'sudo -S {c}'.format(c=cmd) @@ -112,15 +152,15 @@ class SSH(object): def interactive_terminal_open(self, time_out=10): """Open interactive terminal on a new channel on the connected Node. - :param time_out: Timeout in seconds. - :return: SSH channel with opened terminal. + :param time_out: Timeout in seconds. + :return: SSH channel with opened terminal. - .. warning:: Interruptingcow is used here, and it uses - signal(SIGALRM) to let the operating system interrupt program - execution. This has the following limitations: Python signal - handlers only apply to the main thread, so you cannot use this - from other threads. You must not use this in a program that - uses SIGALRM itself (this includes certain profilers) + .. warning:: Interruptingcow is used here, and it uses + signal(SIGALRM) to let the operating system interrupt program + execution. This has the following limitations: Python signal + handlers only apply to the main thread, so you cannot use this + from other threads. You must not use this in a program that + uses SIGALRM itself (this includes certain profilers) """ chan = self._ssh.get_transport().open_session() chan.get_pty() @@ -137,25 +177,26 @@ class SSH(object): raise Exception('Open interactive terminal timeout.') return chan - def interactive_terminal_exec_command(self, chan, cmd, prompt, + @staticmethod + def interactive_terminal_exec_command(chan, cmd, prompt, time_out=10): """Execute command on interactive terminal. - interactive_terminal_open() method has to be called first! - - :param chan: SSH channel with opened terminal. - :param cmd: Command to be executed. - :param prompt: Command prompt, sequence of characters used to - indicate readiness to accept commands. - :param time_out: Timeout in seconds. - :return: Command output. - - .. warning:: Interruptingcow is used here, and it uses - signal(SIGALRM) to let the operating system interrupt program - execution. This has the following limitations: Python signal - handlers only apply to the main thread, so you cannot use this - from other threads. You must not use this in a program that - uses SIGALRM itself (this includes certain profilers) + interactive_terminal_open() method has to be called first! + + :param chan: SSH channel with opened terminal. + :param cmd: Command to be executed. + :param prompt: Command prompt, sequence of characters used to + indicate readiness to accept commands. + :param time_out: Timeout in seconds. + :return: Command output. + + .. warning:: Interruptingcow is used here, and it uses + signal(SIGALRM) to let the operating system interrupt program + execution. This has the following limitations: Python signal + handlers only apply to the main thread, so you cannot use this + from other threads. You must not use this in a program that + uses SIGALRM itself (this includes certain profilers) """ chan.sendall('{c}\n'.format(c=cmd)) buf = '' @@ -169,10 +210,11 @@ class SSH(object): tmp = buf.replace(cmd.replace('\n', ''), '') return tmp.replace(prompt, '') - def interactive_terminal_close(self, chan): + @staticmethod + def interactive_terminal_close(chan): """Close interactive terminal SSH channel. - :param: chan: SSH channel to be closed. + :param: chan: SSH channel to be closed. """ chan.close() @@ -182,7 +224,7 @@ class SSH(object): connect() method has to be called first! """ logger.trace('SCP {0} to {1}:{2}'.format( - local_path, self._hostname, remote_path)) + local_path, self._ssh.get_transport().getpeername(), remote_path)) # SCPCLient takes a paramiko transport as its only argument scp = SCPClient(self._ssh.get_transport()) start = time() @@ -221,15 +263,17 @@ def exec_cmd(node, cmd, timeout=None, sudo=False): logger.error(e) return None - return (ret_code, stdout, stderr) + return ret_code, stdout, stderr + def exec_cmd_no_error(node, cmd, timeout=None, sudo=False): """Convenience function to ssh/exec/return out & err. + Verifies that return code is zero. Returns (stdout, stderr). """ - (rc, stdout, stderr) = exec_cmd(node,cmd, timeout=timeout, sudo=sudo) + (rc, stdout, stderr) = exec_cmd(node, cmd, timeout=timeout, sudo=sudo) assert_equal(rc, 0, 'Command execution failed: "{}"\n{}'. format(cmd, stderr)) - return (stdout, stderr) + return stdout, stderr