Create sshv2 client for compatibility with parallel runner

Change-Id: I5e80177859be74a869dd3d908f516d751258ae7c
This commit is contained in:
Nathan Buckner 2015-11-24 19:07:39 -06:00
parent a56f13e51c
commit af6c42498b
12 changed files with 824 additions and 1 deletions

View File

@ -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__)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -55,7 +55,7 @@ setup(
author='CafeHub',
author_email='cloud-cafe@lists.rackspace.com',
url='http://opencafe.readthedocs.org',
install_requires=['six'],
install_requires=['six', 'pysocks'],
packages=find_packages(exclude=('tests*', 'docs')),
package_data={'cafe': plugins},
license=open('LICENSE').read(),