Create sshv2 client for compatibility with parallel runner
Change-Id: I5e80177859be74a869dd3d908f516d751258ae7c
This commit is contained in:
parent
a56f13e51c
commit
af6c42498b
|
@ -0,0 +1,14 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
"""Declares namespace, required for Opencafe plugins"""
|
||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cafe.engine.config import EngineConfig
|
||||||
|
from cafe.engine.sshv2.common import BaseSSHClass
|
||||||
|
from cafe.engine.sshv2.models import SSHKeyResponse
|
||||||
|
|
||||||
|
ENGINE_CONFIG = EngineConfig()
|
||||||
|
|
||||||
|
|
||||||
|
class SSHBehavior(BaseSSHClass):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_rsa_ssh_keys(cls, key_size=None, pass_phrase=None):
|
||||||
|
key_size = key_size or 2048
|
||||||
|
pass_phrase = pass_phrase or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
private_key = RSA.generate(key_size)
|
||||||
|
public_key = private_key.publickey()
|
||||||
|
except ValueError as exception:
|
||||||
|
cls._log.error("Key Generate exception: \n {0}".format(exception))
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
return SSHKeyResponse(
|
||||||
|
public_key=public_key.exportKey(passphrase=pass_phrase),
|
||||||
|
private_key=private_key.exportKey(passphrase=pass_phrase))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_secure_keys_local(
|
||||||
|
cls, private_key, public_key=None, path=None, file_name=None):
|
||||||
|
if path is None:
|
||||||
|
path = ENGINE_CONFIG.temp_directory
|
||||||
|
if file_name is None:
|
||||||
|
file_name = "id_rsa"
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
key_path = os.path.join(path, file_name)
|
||||||
|
cls.write_file_with_permissions(key_path, private_key, 0o600)
|
||||||
|
key_path = "{0}.pub".format(key_path)
|
||||||
|
cls.write_file_with_permissions(key_path, public_key, 0o600)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_file_with_permissions(file_path, string=None, permissions=0o600):
|
||||||
|
if string is None:
|
||||||
|
return
|
||||||
|
with open(file_path, "w") as file_:
|
||||||
|
file_.write(string)
|
||||||
|
os.chmod(file_path, permissions)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_and_write_files(
|
||||||
|
cls, path=None, file_name=None, key_size=None, passphrase=None):
|
||||||
|
keys = cls.generate_rsa_ssh_keys(key_size, passphrase)
|
||||||
|
cls.write_secure_keys_local(
|
||||||
|
private_key=keys.private_key, public_key=keys.public_key,
|
||||||
|
path=path, file_name=file_name)
|
|
@ -0,0 +1,306 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from socks import socket, create_connection
|
||||||
|
from StringIO import StringIO
|
||||||
|
from uuid import uuid4
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
|
||||||
|
from paramiko import AutoAddPolicy, RSAKey
|
||||||
|
from paramiko.client import SSHClient as ParamikoSSHClient
|
||||||
|
|
||||||
|
from cafe.engine.sshv2.common import (
|
||||||
|
BaseSSHClass, _SSHLogger, DEFAULT_TIMEOUT, POLLING_RATE, CHANNEL_KEEPALIVE)
|
||||||
|
from cafe.engine.sshv2.models import ExecResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyTypes(object):
|
||||||
|
SOCKS5 = 2
|
||||||
|
SOCKS4 = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedParamikoSSHClient(ParamikoSSHClient):
|
||||||
|
def execute_command(
|
||||||
|
self, command, bufsize=-1, timeout=None, stdin_str="", stdin_file=None,
|
||||||
|
raise_exceptions=False):
|
||||||
|
timeout = timeout or DEFAULT_TIMEOUT
|
||||||
|
chan = self._transport.open_session()
|
||||||
|
chan.settimeout(POLLING_RATE)
|
||||||
|
chan.exec_command(command)
|
||||||
|
stdin_str = stdin_str if stdin_file is None else stdin_file.read()
|
||||||
|
stdin = chan.makefile("wb", bufsize)
|
||||||
|
stdout = chan.makefile("rb", bufsize)
|
||||||
|
stderr = chan.makefile_stderr("rb", bufsize)
|
||||||
|
stdin.write(stdin_str)
|
||||||
|
stdin.close()
|
||||||
|
stdout_str = stderr_str = ""
|
||||||
|
exit_status = None
|
||||||
|
max_time = time.time() + timeout
|
||||||
|
while not chan.exit_status_ready():
|
||||||
|
stderr_str += self._read_channel(stderr)
|
||||||
|
stdout_str += self._read_channel(stdout)
|
||||||
|
if max_time < time.time():
|
||||||
|
raise socket.timeout(
|
||||||
|
"Command timed out\nSTDOUT:{0}\nSTDERR:{1}\n".format(
|
||||||
|
stdout_str, stderr_str))
|
||||||
|
exit_status = chan.recv_exit_status()
|
||||||
|
stdout_str += self._read_channel(stdout)
|
||||||
|
stderr_str += self._read_channel(stderr)
|
||||||
|
chan.close()
|
||||||
|
return stdin_str, stdout_str, stderr_str, exit_status
|
||||||
|
|
||||||
|
def _read_channel(self, chan):
|
||||||
|
read = ""
|
||||||
|
try:
|
||||||
|
read += chan.read()
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return read
|
||||||
|
|
||||||
|
|
||||||
|
class SSHClient(BaseSSHClass):
|
||||||
|
def __init__(
|
||||||
|
self, hostname=None, port=22, username=None, password=None,
|
||||||
|
accept_missing_host_key=True, timeout=None, compress=True, pkey=None,
|
||||||
|
look_for_keys=False, allow_agent=False, key_filename=None,
|
||||||
|
proxy_type=None, proxy_ip=None, proxy_port=None, sock=None):
|
||||||
|
super(SSHClient, self).__init__()
|
||||||
|
self.connect_kwargs = {}
|
||||||
|
self.accept_missing_host_key = accept_missing_host_key
|
||||||
|
self.proxy_port = proxy_port
|
||||||
|
self.proxy_ip = proxy_ip
|
||||||
|
self.proxy_type = proxy_type
|
||||||
|
self.connect_kwargs["timeout"] = timeout or DEFAULT_TIMEOUT
|
||||||
|
self.connect_kwargs["hostname"] = hostname
|
||||||
|
self.connect_kwargs["port"] = int(port)
|
||||||
|
self.connect_kwargs["username"] = username
|
||||||
|
self.connect_kwargs["password"] = password
|
||||||
|
self.connect_kwargs["compress"] = compress
|
||||||
|
self.connect_kwargs["pkey"] = pkey
|
||||||
|
self.connect_kwargs["look_for_keys"] = look_for_keys
|
||||||
|
self.connect_kwargs["allow_agent"] = allow_agent
|
||||||
|
self.connect_kwargs["key_filename"] = key_filename
|
||||||
|
self.connect_kwargs["sock"] = sock
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeout(self):
|
||||||
|
return self.connect_kwargs.get("timeout")
|
||||||
|
|
||||||
|
@timeout.setter
|
||||||
|
def timeout(self, value):
|
||||||
|
self.connect_kwargs["timeout"] = value
|
||||||
|
|
||||||
|
@timeout.deleter
|
||||||
|
def timeout(self):
|
||||||
|
if "timeout" in self.connect_kwargs:
|
||||||
|
del self.connect_kwargs["timeout"]
|
||||||
|
|
||||||
|
def _connect(
|
||||||
|
self, hostname=None, port=None, username=None, password=None,
|
||||||
|
accept_missing_host_key=None, timeout=None, compress=None, pkey=None,
|
||||||
|
look_for_keys=None, allow_agent=None, key_filename=None,
|
||||||
|
proxy_type=None, proxy_ip=None, proxy_port=None, sock=None):
|
||||||
|
connect_kwargs = dict(self.connect_kwargs)
|
||||||
|
connect_kwargs.update({
|
||||||
|
k: locals().get(k) for k in self.connect_kwargs
|
||||||
|
if locals().get(k) is not None})
|
||||||
|
connect_kwargs["port"] = int(connect_kwargs.get("port"))
|
||||||
|
|
||||||
|
ssh = ExtendedParamikoSSHClient()
|
||||||
|
|
||||||
|
if bool(self.accept_missing_host_key or accept_missing_host_key):
|
||||||
|
ssh.set_missing_host_key_policy(AutoAddPolicy())
|
||||||
|
|
||||||
|
if connect_kwargs.get("pkey") is not None:
|
||||||
|
connect_kwargs["pkey"] = RSAKey.from_private_key(
|
||||||
|
io.StringIO(unicode(connect_kwargs["pkey"])))
|
||||||
|
|
||||||
|
proxy_type = proxy_type or self.proxy_type
|
||||||
|
proxy_ip = proxy_ip or self.proxy_ip
|
||||||
|
proxy_port = proxy_port or self.proxy_port
|
||||||
|
if connect_kwargs.get("sock") is not None:
|
||||||
|
pass
|
||||||
|
elif all([proxy_type, proxy_ip, proxy_port]):
|
||||||
|
connect_kwargs["sock"] = create_connection(
|
||||||
|
(connect_kwargs.get("hostname"), connect_kwargs.get("port")),
|
||||||
|
proxy_type, proxy_ip, int(proxy_port))
|
||||||
|
|
||||||
|
ssh.connect(**connect_kwargs)
|
||||||
|
return ssh
|
||||||
|
|
||||||
|
@_SSHLogger
|
||||||
|
def execute_command(
|
||||||
|
self, command, bufsize=-1, get_pty=False,
|
||||||
|
stdin_str="", stdin_file=None, **connect_kwargs):
|
||||||
|
ssh_client = self._connect(**connect_kwargs)
|
||||||
|
stdin, stdout, stderr, exit_status = ssh_client.execute_command(
|
||||||
|
timeout=self._get_timeout(connect_kwargs.get("timeout")),
|
||||||
|
command=command, bufsize=bufsize)
|
||||||
|
ssh_client.close()
|
||||||
|
del ssh_client
|
||||||
|
return ExecResponse(
|
||||||
|
stdin=stdin, stdout=stdout, stderr=stderr, exit_status=exit_status)
|
||||||
|
|
||||||
|
def _get_timeout(self, timeout=None):
|
||||||
|
return timeout if timeout is not None else self.timeout
|
||||||
|
|
||||||
|
@_SSHLogger
|
||||||
|
def create_shell(self, keepalive=None, **connect_kwargs):
|
||||||
|
connection = self._connect(**connect_kwargs)
|
||||||
|
return SSHShell(
|
||||||
|
connection, self._get_timeout(connect_kwargs.get("timeout")),
|
||||||
|
keepalive)
|
||||||
|
|
||||||
|
@_SSHLogger
|
||||||
|
def create_sftp(self, keepalive=None, **connect_kwargs):
|
||||||
|
connection = self._connect(**connect_kwargs)
|
||||||
|
return SFTPShell(connection, keepalive)
|
||||||
|
|
||||||
|
|
||||||
|
class SFTPShell(BaseSSHClass):
|
||||||
|
def __init__(self, connection=None, keepalive=None):
|
||||||
|
super(SFTPShell, self).__init__()
|
||||||
|
self.connection = connection
|
||||||
|
self.sftp = connection.open_sftp()
|
||||||
|
self.sftp.get_channel().get_transport().set_keepalive(
|
||||||
|
keepalive or CHANNEL_KEEPALIVE)
|
||||||
|
self.chdir(".")
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
if name in [
|
||||||
|
"chdir", "chmod", "chown", "file", "get", "getcwd", "getfo",
|
||||||
|
"listdir", "listdir_attr", "listdir_iter", "lstat", "mkdir",
|
||||||
|
"normalize", "open", "put", "putfo", "readlink", "remove",
|
||||||
|
"rename", "rmdir", "stat", "symlink", "truncate", "unlink",
|
||||||
|
"utime"]:
|
||||||
|
return self.sftp.__getattribute__(name)
|
||||||
|
else:
|
||||||
|
return super(SFTPShell, self).__getattribute__(name)
|
||||||
|
|
||||||
|
def exists(self, path):
|
||||||
|
ret_val = False
|
||||||
|
try:
|
||||||
|
self.sftp.stat(path)
|
||||||
|
ret_val = True
|
||||||
|
except IOError, e:
|
||||||
|
if e[0] != 2:
|
||||||
|
raise
|
||||||
|
ret_val = False
|
||||||
|
return ret_val
|
||||||
|
|
||||||
|
def get_file(self, remote_path):
|
||||||
|
ret_val = StringIO()
|
||||||
|
self.getfo(ret_val, remote_path)
|
||||||
|
return ret_val.getvalue()
|
||||||
|
|
||||||
|
def write_file(self, data, remote_path):
|
||||||
|
return self.putfo(StringIO(data), remote_path)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if hasattr(self, "sftp"):
|
||||||
|
self.sftp.close()
|
||||||
|
del self.sftp
|
||||||
|
if hasattr(self, "connection"):
|
||||||
|
self.connection.close()
|
||||||
|
del self.connection
|
||||||
|
|
||||||
|
|
||||||
|
class SSHShell(BaseSSHClass):
|
||||||
|
RAISE = "RAISE"
|
||||||
|
RAISE_DISCONNECT = "RAISE_DISCONNECT"
|
||||||
|
|
||||||
|
def __init__(self, connection, timeout, keepalive=None):
|
||||||
|
super(SSHShell, self).__init__()
|
||||||
|
self.connection = connection
|
||||||
|
self.timeout = timeout
|
||||||
|
self.channel = self._create_channel()
|
||||||
|
self.channel.settimeout(POLLING_RATE)
|
||||||
|
self._clear_channel()
|
||||||
|
self.channel.get_transport().set_keepalive(
|
||||||
|
keepalive or CHANNEL_KEEPALIVE)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if hasattr(self, "channel"):
|
||||||
|
self.channel.close()
|
||||||
|
del self.channel
|
||||||
|
if hasattr(self, "connection"):
|
||||||
|
self.connection.close()
|
||||||
|
del self.connection
|
||||||
|
|
||||||
|
@_SSHLogger
|
||||||
|
def execute_shell_command(
|
||||||
|
self, cmd, timeout=None, timeout_action=RAISE_DISCONNECT,
|
||||||
|
exception_on_timeout=True):
|
||||||
|
max_time = time.time() + self._get_timeout(timeout)
|
||||||
|
uuid = str(uuid4()).replace("-", "")
|
||||||
|
cmd = "echo {1}\n{0}\necho {1} $?\n".format(cmd.strip(), uuid)
|
||||||
|
try:
|
||||||
|
self._clear_channel()
|
||||||
|
self._wait_for_active_shell(max_time)
|
||||||
|
self.channel.send(cmd)
|
||||||
|
response = self._read_shell_response(uuid, max_time)
|
||||||
|
except socket.timeout as e:
|
||||||
|
if timeout_action == self.RAISE_DISCONNECT:
|
||||||
|
self.close()
|
||||||
|
raise e
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _create_channel(self):
|
||||||
|
chan = self.connection._transport.open_session()
|
||||||
|
chan.invoke_shell()
|
||||||
|
return chan
|
||||||
|
|
||||||
|
def _wait_for_active_shell(self, max_time):
|
||||||
|
while not self.channel.send_ready():
|
||||||
|
time.sleep(POLLING_RATE)
|
||||||
|
if max_time < time.time():
|
||||||
|
raise socket.timeout("Timed out waiting for active shell")
|
||||||
|
|
||||||
|
def _read_shell_response(self, uuid, max_time):
|
||||||
|
stdout = stderr = ""
|
||||||
|
exit_status = None
|
||||||
|
while max_time > time.time():
|
||||||
|
stdout += self._read_channel(self.channel.recv)
|
||||||
|
stderr += self._read_channel(self.channel.recv_stderr)
|
||||||
|
if stdout.count(uuid) == 2:
|
||||||
|
list_ = stdout.split(uuid)
|
||||||
|
stdout = list_[1]
|
||||||
|
try:
|
||||||
|
exit_status = int(list_[2])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
exit_status = None
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise socket.timeout(
|
||||||
|
"Command timed out\nSTDOUT:{0}\nSTDERR:{1}\n".format(
|
||||||
|
stdout, stderr))
|
||||||
|
response = ExecResponse(
|
||||||
|
stdin=None, stdout=stdout.strip(), stderr=stderr,
|
||||||
|
exit_status=exit_status)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _read_channel(self, read_func, buffsize=1024):
|
||||||
|
read = ""
|
||||||
|
try:
|
||||||
|
read += read_func(buffsize)
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return read
|
||||||
|
|
||||||
|
def _clear_channel(self):
|
||||||
|
self._read_channel(self.channel.recv)
|
||||||
|
self._read_channel(self.channel.recv_stderr)
|
||||||
|
|
||||||
|
def _get_timeout(self, timeout=None):
|
||||||
|
return timeout if timeout is not None else self.timeout
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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 functools
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cafe.common.reporting import cclogging
|
||||||
|
from cafe.engine.sshv2.models import ExecResponse
|
||||||
|
CHANNEL_KEEPALIVE = 45
|
||||||
|
DEFAULT_TIMEOUT = 60
|
||||||
|
POLLING_RATE = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def _SSHLogger(func):
|
||||||
|
DASH_WIDTH = 42
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
message = (
|
||||||
|
"{equals}\nCALL\n{dash}\n"
|
||||||
|
"{name} args..........: {args}\n"
|
||||||
|
"{name} kwargs........: {kwargs}\n"
|
||||||
|
"{dash}\n").format(
|
||||||
|
dash="-" * DASH_WIDTH, equals="=" * DASH_WIDTH, name=func.__name__,
|
||||||
|
args=args, kwargs=kwargs)
|
||||||
|
self._log.info(message)
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
resp = func(self, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
self._log.critical(e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
if isinstance(resp, ExecResponse):
|
||||||
|
message = (
|
||||||
|
"{equals}\nRESPONSE\n{dash}\n"
|
||||||
|
"response stdout......: {stdout}\n"
|
||||||
|
"response stderr......: {stderr}\n"
|
||||||
|
"response exit_status.: {exit_status}\n"
|
||||||
|
"response elapsed.....: {elapsed}\n"
|
||||||
|
"{dash}\n").format(
|
||||||
|
dash="-" * 42, equals="=" * 42, elapsed=elapsed,
|
||||||
|
stdout=resp.stdout.rstrip("\n"),
|
||||||
|
stderr=resp.stderr.rstrip("\n"), exit_status=resp.exit_status)
|
||||||
|
self._log.info(message)
|
||||||
|
return resp
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class ClassPropertyDescriptor(object):
|
||||||
|
|
||||||
|
def __init__(self, fget, fset=None):
|
||||||
|
self.fget = fget
|
||||||
|
self.fset = fset
|
||||||
|
|
||||||
|
def __get__(self, obj, klass=None):
|
||||||
|
if klass is None:
|
||||||
|
klass = type(obj)
|
||||||
|
return self.fget.__get__(obj, klass)()
|
||||||
|
|
||||||
|
|
||||||
|
def classproperty(func):
|
||||||
|
if not isinstance(func, (classmethod, staticmethod)):
|
||||||
|
func = classmethod(func)
|
||||||
|
return ClassPropertyDescriptor(func)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSSHClass(object):
|
||||||
|
@classproperty
|
||||||
|
def _log(cls):
|
||||||
|
return cclogging.getLogger(cclogging.get_object_namespace(cls))
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exception_type, exception_value, traceback):
|
||||||
|
if getattr(self, "close", False):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if getattr(self, "close", False):
|
||||||
|
self.close()
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from cafe.engine.sshv2.behaviors import SSHBehavior
|
||||||
|
from cafe.engine.sshv2.client import SSHClient
|
||||||
|
from cafe.engine.sshv2.common import BaseSSHClass
|
||||||
|
from cafe.engine.sshv2.config import ProxyConfig, SSHClientConfig
|
||||||
|
from cafe.engine.sshv2.proxy import SSHProxy
|
||||||
|
|
||||||
|
|
||||||
|
class SSHComposite(BaseSSHClass):
|
||||||
|
def __init__(self, ssh_config=None, proxy_config=None):
|
||||||
|
self.proxy_config = proxy_config or ProxyConfig()
|
||||||
|
self.ssh_config = ssh_config or SSHClientConfig()
|
||||||
|
|
||||||
|
self.proxy_client = SSHProxy(
|
||||||
|
hostname=self.proxy_config.hostname,
|
||||||
|
port=self.proxy_config.port,
|
||||||
|
username=self.proxy_config.username,
|
||||||
|
compress=self.proxy_config.compress,
|
||||||
|
look_for_keys=self.proxy_config.look_for_keys,
|
||||||
|
key_filename=self.proxy_config.key_filename)
|
||||||
|
|
||||||
|
self.client = SSHClient(
|
||||||
|
hostname=self.ssh_config.hostname,
|
||||||
|
port=self.ssh_config.port,
|
||||||
|
username=self.ssh_config.username,
|
||||||
|
password=self.ssh_config.password,
|
||||||
|
accept_missing_host_key=self.ssh_config.accept_missing_host_key,
|
||||||
|
timeout=self.ssh_config.timeout,
|
||||||
|
compress=self.ssh_config.compress,
|
||||||
|
pkey=self.ssh_config.pkey,
|
||||||
|
look_for_keys=self.ssh_config.look_for_keys,
|
||||||
|
allow_agent=self.ssh_config.allow_agent,
|
||||||
|
key_filename=self.ssh_config.key_filename,
|
||||||
|
proxy_type=self.ssh_config.proxy_type,
|
||||||
|
proxy_ip=self.ssh_config.proxy_ip,
|
||||||
|
proxy_port=self.ssh_config.proxy_port)
|
||||||
|
|
||||||
|
self.behavior = SSHBehavior
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from cafe.engine.models.data_interfaces import ConfigSectionInterface
|
||||||
|
from cafe.engine.sshv2.common import DEFAULT_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
class SSHClientConfig(ConfigSectionInterface):
|
||||||
|
SECTION_NAME = "ssh_client"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hostname(self):
|
||||||
|
return self.get("hostname", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
return self.get("port", 22)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
return self.get("username", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
return self.get_raw("password")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accept_missing_host_key(self):
|
||||||
|
return self.get_boolean("accept_missing_host_key", True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeout(self):
|
||||||
|
return self.get("timeout", DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def compress(self):
|
||||||
|
return self.get_boolean("compress", True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pkey(self):
|
||||||
|
return self.get("pkey", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def look_for_keys(self):
|
||||||
|
return self.get_boolean("look_for_keys", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allow_agent(self):
|
||||||
|
return self.get_boolean("allow_agent", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_filename(self):
|
||||||
|
return self.get("key_filename")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_type(self):
|
||||||
|
return self.get("proxy_type")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_ip(self):
|
||||||
|
return self.get("proxy_ip")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def proxy_port(self):
|
||||||
|
return self.get("proxy_port")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyConfig(ConfigSectionInterface):
|
||||||
|
SECTION_NAME = "proxy"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hostname(self):
|
||||||
|
return self.get("hostname", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
return self.get("port", 22)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
return self.get("username", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def compress(self):
|
||||||
|
return self.get_boolean("compress", True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def look_for_keys(self):
|
||||||
|
return self.get_boolean("look_for_keys", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_filename(self):
|
||||||
|
return self.get("key_filename")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyForward(ConfigSectionInterface):
|
||||||
|
SECTION_NAME = "port_forward"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bind_port(self):
|
||||||
|
return self.get("bind_port", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forward_hostname(self):
|
||||||
|
return self.get("forward_hostname", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forward_port(self):
|
||||||
|
return self.get("forward_port", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bind_address(self):
|
||||||
|
return self.get("bind_address", None)
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from cafe.engine.models.base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ExecResponse(BaseModel):
|
||||||
|
def __init__(
|
||||||
|
self, stdin=None, stdout=None, stderr=None, exit_status=None):
|
||||||
|
self.stdin = stdin
|
||||||
|
self.stdout = stdout
|
||||||
|
self.stderr = stderr
|
||||||
|
self.exit_status = exit_status
|
||||||
|
|
||||||
|
|
||||||
|
class SSHKeyResponse(BaseModel):
|
||||||
|
def __init__(
|
||||||
|
self, public_key=None, private_key=None, error=None):
|
||||||
|
self.public_key = public_key
|
||||||
|
self.private_key = private_key
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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 os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from cafe.engine.sshv2.common import BaseSSHClass
|
||||||
|
|
||||||
|
|
||||||
|
class SSHProxy(BaseSSHClass):
|
||||||
|
def __init__(
|
||||||
|
self, hostname=None, port=22, username=None, compress=True,
|
||||||
|
look_for_keys=False, key_filename=None):
|
||||||
|
super(SSHProxy, self).__init__()
|
||||||
|
for k, v in locals().items():
|
||||||
|
if k != "self":
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def _get_args(
|
||||||
|
self, hostname=None, port=None, username=None, compress=None,
|
||||||
|
look_for_keys=None, key_filename=None):
|
||||||
|
args = [
|
||||||
|
"ssh", "-oUserKnownHostsFile=/dev/null",
|
||||||
|
"-oStrictHostKeyChecking=no", "-oExitOnForwardFailure=yes", "-N"]
|
||||||
|
hostname = hostname or self.hostname
|
||||||
|
username = username or self.username
|
||||||
|
compress = compress if compress is not None else self.compress
|
||||||
|
key_filename = key_filename or self.key_filename
|
||||||
|
look_for_keys = (
|
||||||
|
look_for_keys if look_for_keys is not None
|
||||||
|
else self.look_for_keys)
|
||||||
|
|
||||||
|
args.append("-P{0}".format(port or self.port))
|
||||||
|
if compress:
|
||||||
|
args.append("-C")
|
||||||
|
|
||||||
|
if look_for_keys is False:
|
||||||
|
args.append("-i{0}".format(key_filename))
|
||||||
|
|
||||||
|
if username:
|
||||||
|
args.append("{0}@{1}".format(username, hostname))
|
||||||
|
else:
|
||||||
|
args.append(hostname)
|
||||||
|
return args
|
||||||
|
|
||||||
|
def create_forward_port(
|
||||||
|
self, bind_port, forward_hostname, forward_port, bind_address=None,
|
||||||
|
**connect_kwargs):
|
||||||
|
args = self._get_args(**connect_kwargs)
|
||||||
|
if bind_address:
|
||||||
|
args.append("-L{0}:{1}:{2}:{3}".format(
|
||||||
|
bind_address, bind_port, forward_hostname, forward_port))
|
||||||
|
else:
|
||||||
|
args.append("-L{0}:{1}:{2}".format(
|
||||||
|
bind_port, forward_hostname, forward_port))
|
||||||
|
return PortForward(
|
||||||
|
subprocess.Popen(args).pid, "forward", bind_address or "localhost",
|
||||||
|
bind_port)
|
||||||
|
|
||||||
|
def create_socks_proxy(
|
||||||
|
self, bind_port, bind_address=None, **connect_kwargs):
|
||||||
|
args = self._get_args(**connect_kwargs)
|
||||||
|
if bind_address:
|
||||||
|
args.append("-D{0}:{1}".format(bind_address, bind_port))
|
||||||
|
else:
|
||||||
|
args.append("-D{0}:{1}:{2}".format(bind_port))
|
||||||
|
return SocksProxy(
|
||||||
|
subprocess.Popen(args).pid, "forward", bind_address or "localhost",
|
||||||
|
bind_port)
|
||||||
|
|
||||||
|
|
||||||
|
class PortForward(BaseSSHClass):
|
||||||
|
def __init__(self, pid, type_, bind_address, bind_port):
|
||||||
|
for k, v in locals().items():
|
||||||
|
if k != "self":
|
||||||
|
setattr(self, k, v)
|
||||||
|
self.name = None
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, signal.SIGKILL)
|
||||||
|
except OSError as e:
|
||||||
|
self._log.warning(
|
||||||
|
"Close called more than once or process ended unexpectedly")
|
||||||
|
self._log.warning(e)
|
||||||
|
|
||||||
|
|
||||||
|
class SocksProxy(PortForward):
|
||||||
|
pass
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Copyright 2015 Rackspace
|
||||||
|
# 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.
|
||||||
|
"""A setuptools based setup module.
|
||||||
|
See:
|
||||||
|
https://packaging.python.org/en/latest/distributing.html
|
||||||
|
"""
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='cafe_sshv2_plugin',
|
||||||
|
version='0.0.1',
|
||||||
|
description='Paramiko based plugin for OpenCAFE',
|
||||||
|
author='Rackspace Cloud QE',
|
||||||
|
author_email='cloud-cafe@lists.rackspace.com',
|
||||||
|
url='http://rackspace.com',
|
||||||
|
packages=find_packages(),
|
||||||
|
namespace_packages=['cafe'],
|
||||||
|
install_requires=['paramiko', 'pysocks'],
|
||||||
|
zip_safe=False)
|
2
setup.py
2
setup.py
|
@ -55,7 +55,7 @@ setup(
|
||||||
author='CafeHub',
|
author='CafeHub',
|
||||||
author_email='cloud-cafe@lists.rackspace.com',
|
author_email='cloud-cafe@lists.rackspace.com',
|
||||||
url='http://opencafe.readthedocs.org',
|
url='http://opencafe.readthedocs.org',
|
||||||
install_requires=['six'],
|
install_requires=['six', 'pysocks'],
|
||||||
packages=find_packages(exclude=('tests*', 'docs')),
|
packages=find_packages(exclude=('tests*', 'docs')),
|
||||||
package_data={'cafe': plugins},
|
package_data={'cafe': plugins},
|
||||||
license=open('LICENSE').read(),
|
license=open('LICENSE').read(),
|
||||||
|
|
Loading…
Reference in New Issue