
Change-Id: I1c7cdbbf16c137a6739447d2776595725b798b54 Signed-off-by: Stefan Kobza <skobza@cisco.com>
128 lines
4.0 KiB
Python
128 lines
4.0 KiB
Python
# Copyright (c) 2015 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
|
|
|
|
__all__ = ["exec_cmd"]
|
|
|
|
# TODO: Attempt to recycle SSH connections
|
|
# 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).
|
|
"""
|
|
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 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):
|
|
"""Convenience function to ssh/exec/return rc & out.
|
|
|
|
Returns (rc, stdout).
|
|
"""
|
|
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:
|
|
(ret_code, stdout, stderr) = ssh.exec_command(cmd, timeout=timeout)
|
|
except Exception, e:
|
|
logger.error(e)
|
|
return None
|
|
|
|
return (ret_code, stdout, stderr)
|
|
|