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_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(),
|
||||
|
|
Loading…
Reference in New Issue