diff options
Diffstat (limited to 'resources/libraries/python/ssh.py')
-rw-r--r-- | resources/libraries/python/ssh.py | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/resources/libraries/python/ssh.py b/resources/libraries/python/ssh.py new file mode 100644 index 0000000000..72e41c76a6 --- /dev/null +++ b/resources/libraries/python/ssh.py @@ -0,0 +1,235 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 paramiko +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 + +__all__ = ["exec_cmd", "exec_cmd_no_error"] + +# TODO: load priv key + + +class SSH(object): + + __MAX_RECV_BUF = 10*1024*1024 + __existing_connections = {} + + def __init__(self): + self._ssh = paramiko.SSHClient() + self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self._hostname = None + + def _node_hash(self, node): + return hash(frozenset([node['host'], node['port']])) + + def connect(self, node): + """Connect to node prior to running exec_command or scp. + + 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] + else: + start = time() + self._ssh.connect(node['host'], username=node['username'], + password=node['password']) + self.__existing_connections[node_hash] = self._ssh + logger.trace('connect took {} seconds'.format(time() - start)) + + 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)) + 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)) + + stdout = "" + while True: + buf = chan.recv(self.__MAX_RECV_BUF) + stdout += buf + if not buf: + break + + stderr = "" + while True: + buf = chan.recv_stderr(self.__MAX_RECV_BUF) + stderr += buf + if not buf: + break + + return_code = chan.recv_exit_status() + logger.trace('chan_recv/_stderr took {} seconds'.format(time()-end)) + + 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 + + :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("vpe_api_test", "dump_interface_table") + """ + if cmd_input is None: + command = 'sudo -S {c}'.format(c=cmd) + else: + command = 'sudo -S {c} <<< "{i}"'.format(c=cmd, i=cmd_input) + return self.exec_command(command, timeout) + + 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. + + .. 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() + chan.invoke_shell() + chan.settimeout(int(time_out)) + + buf = '' + try: + with timeout(time_out, exception=RuntimeError): + while not buf.endswith(':~$ '): + if chan.recv_ready(): + buf = chan.recv(4096) + except RuntimeError: + raise Exception('Open interactive terminal timeout.') + return chan + + def interactive_terminal_exec_command(self, 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) + """ + chan.sendall('{c}\n'.format(c=cmd)) + buf = '' + try: + with timeout(time_out, exception=RuntimeError): + while not buf.endswith(prompt): + if chan.recv_ready(): + buf += chan.recv(4096) + except RuntimeError: + raise Exception("Exec '{c}' timeout.".format(c=cmd)) + tmp = buf.replace(cmd.replace('\n', ''), '') + return tmp.replace(prompt, '') + + def interactive_terminal_close(self, chan): + """Close interactive terminal SSH channel. + + :param: chan: SSH channel to be closed. + """ + chan.close() + + def scp(self, local_path, remote_path): + """Copy files from local_path to remote_path. + + connect() method has to be called first! + """ + logger.trace('SCP {0} to {1}:{2}'.format( + local_path, self._hostname, remote_path)) + # SCPCLient takes a paramiko transport as its only argument + scp = SCPClient(self._ssh.get_transport()) + start = time() + scp.put(local_path, remote_path) + scp.close() + end = time() + logger.trace('SCP took {0} seconds'.format(end-start)) + + +def exec_cmd(node, cmd, timeout=None, sudo=False): + """Convenience function to ssh/exec/return rc, out & err. + + Returns (rc, stdout, stderr). + """ + if node is None: + raise TypeError('Node parameter is None') + if cmd is None: + raise TypeError('Command parameter is None') + if len(cmd) == 0: + raise ValueError('Empty command parameter') + + ssh = SSH() + try: + ssh.connect(node) + except Exception, e: + logger.error("Failed to connect to node" + e) + return None + + try: + if not sudo: + (ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=timeout) + else: + (ret_code, stdout, stderr) = ssh.exec_command_sudo(cmd, + timeout=timeout) + except Exception, e: + logger.error(e) + return None + + 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) + assert_equal(rc, 0, 'Command execution failed: "{}"\n{}'. + format(cmd, stderr)) + return (stdout, stderr) |