Add SSH support to LeftHand Client
SSH is needed to make Remote Copy related calls to a LeftHand array. Currently, SSH support is not available in the client. This patch adds such capabilities. Change-Id: I50ff9e31c3d3fac97bbabb95880346795f3b4656
This commit is contained in:
parent
e8a8478c1b
commit
84de6562e1
11
README.rst
11
README.rst
|
@ -1,9 +1,10 @@
|
|||
HPE LeftHand/StoreVirtual REST Client
|
||||
===================
|
||||
This is a Client library that can talk to the HPE LeftHand/StoreVirtual Storage array.
|
||||
The HPE LeftHand storage array has a REST web service interface.
|
||||
The HPE LeftHand storage array has a REST web service interface as well as runs SSH.
|
||||
This client library implements a simple interface to talk with that REST
|
||||
interface using the python Requests http library.
|
||||
interface using the python Requests http library and communicates via SSH using
|
||||
Pytohn's paramiko library.
|
||||
|
||||
This is the new location for the rebranded HP LeftHand/StoreVirtual REST Client and
|
||||
will be where all future releases are made. It was previously located on PyPi at:
|
||||
|
@ -94,4 +95,10 @@ Running Simulators
|
|||
|
||||
Manually run flask server (when config.ini unit=true)::
|
||||
|
||||
* WSAPI::
|
||||
|
||||
$ python test/HPELeftHandMockServer_flask.py -port 5001 -user <USERNAME> -password <PASSWORD> -debug
|
||||
|
||||
* SSH::
|
||||
|
||||
$ python test/HPELeftHandMockServer_ssh.py [port]
|
||||
|
|
|
@ -95,3 +95,4 @@ Changes in Version 2.0.1
|
|||
* Changes the exception isinstance check to look for basestring/str instead of
|
||||
bytes in order to properly store the error description.
|
||||
* Adds a new API for modifying snapshots.
|
||||
* Adds SSH support
|
||||
|
|
|
@ -41,6 +41,9 @@ Doing so is easy:
|
|||
# cl = client.HPELeftHandClient("https://10.10.10.10:8081/lhos",
|
||||
# secure='/etc/ssl/certs/ca-certificates.crt')
|
||||
|
||||
# Set the SSH authentication options for the SSH based calls.
|
||||
cl.setSSHOptions(ip_address, username, password)
|
||||
|
||||
try:
|
||||
cl.login(username, password)
|
||||
print "Login worked!"
|
||||
|
|
|
@ -22,7 +22,8 @@ HPELeftHand REST Client
|
|||
|
||||
:Author: Kurt Martin
|
||||
:Description: This is the LeftHand/StoreVirtual Client that talks to the
|
||||
LeftHand OS REST Service.
|
||||
LeftHand OS REST Service. This version also supports running actions on the
|
||||
LeftHand that use SSH.
|
||||
|
||||
This client requires and works with version 11.5 of the LeftHand firmware
|
||||
|
||||
|
@ -35,7 +36,7 @@ except ImportError:
|
|||
# Fall back to Python 2's urllib2
|
||||
from urllib2 import quote
|
||||
|
||||
from hpelefthandclient import exceptions, http
|
||||
from hpelefthandclient import exceptions, http, ssh
|
||||
|
||||
|
||||
class HPELeftHandClient(object):
|
||||
|
@ -47,9 +48,47 @@ class HPELeftHandClient(object):
|
|||
self.api_url = api_url
|
||||
self.http = http.HTTPJSONRESTClient(self.api_url, secure=secure)
|
||||
self.api_version = None
|
||||
self.ssh = None
|
||||
|
||||
self.debug_rest(debug)
|
||||
|
||||
def setSSHOptions(self, ip, login, password, port=16022,
|
||||
conn_timeout=None, privatekey=None,
|
||||
**kwargs):
|
||||
"""Set SSH Options for ssh calls.
|
||||
|
||||
This is used to set the SSH credentials for calls
|
||||
that use SSH instead of REST HTTP.
|
||||
|
||||
:param ip: The IP address of the LeftHand array
|
||||
:type ip: str
|
||||
:param login: Username to log into SSH
|
||||
:type login: str
|
||||
:param password: Password to log into SSH
|
||||
:type password: str
|
||||
:param port: Port the SSH service is running on. The default port
|
||||
is 16022
|
||||
:type port: int
|
||||
:param conn_timeout: The connection timeout in seconds. Default is no
|
||||
connection timeout.
|
||||
:type conn_timeout: int
|
||||
:param privatekey: File location of SSH private key. Default does not
|
||||
use a private key.
|
||||
:type privatekey: int
|
||||
|
||||
"""
|
||||
self.ssh = ssh.HPELeftHandSSHClient(ip, login, password, port,
|
||||
conn_timeout, privatekey,
|
||||
**kwargs)
|
||||
|
||||
def _run(self, cmd):
|
||||
if self.ssh is None:
|
||||
raise exceptions.SSHException('SSH is not initialized. Initialize'
|
||||
' it by calling "setSSHOptions".')
|
||||
else:
|
||||
self.ssh.open()
|
||||
return self.ssh.run(cmd)
|
||||
|
||||
def debug_rest(self, flag):
|
||||
"""
|
||||
This is useful for debugging requests to LeftHand
|
||||
|
@ -59,6 +98,8 @@ class HPELeftHandClient(object):
|
|||
|
||||
"""
|
||||
self.http.set_debug_flag(flag)
|
||||
if self.ssh:
|
||||
self.ssh.set_debug_flag(flag)
|
||||
|
||||
def login(self, username, password):
|
||||
"""
|
||||
|
@ -98,6 +139,8 @@ class HPELeftHandClient(object):
|
|||
|
||||
"""
|
||||
self.http.unauthenticate()
|
||||
if self.ssh:
|
||||
self.ssh.close()
|
||||
|
||||
def getApiVersion(self):
|
||||
"""
|
||||
|
|
|
@ -21,9 +21,13 @@ Exceptions for the client
|
|||
|
||||
:Author: Walter A. Boring IV
|
||||
:Description: This contains the HTTP exceptions that can come back from
|
||||
the REST calls
|
||||
the REST/SSH calls
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# Python 3+ override
|
||||
try:
|
||||
basestring
|
||||
|
@ -413,3 +417,39 @@ def from_response(response, body):
|
|||
"""
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
return cls(body)
|
||||
|
||||
|
||||
class SSHException(Exception):
|
||||
"""This is the basis for the SSH Exceptions."""
|
||||
|
||||
code = 500
|
||||
message = "An unknown exception occurred."
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
|
||||
except Exception:
|
||||
# kwargs doesn't match a variable in the message
|
||||
# log the issue and the kwargs
|
||||
LOG.exception('Exception in string format operation')
|
||||
for name, value in list(kwargs.items()):
|
||||
LOG.error("%s: %s" % (name, value))
|
||||
# at least get the core message out if something happened
|
||||
message = self.message
|
||||
|
||||
self.msg = message
|
||||
super(SSHException, self).__init__(message)
|
||||
|
||||
|
||||
class SSHInjectionThreat(SSHException):
|
||||
message = "SSH command injection detected: %(command)s"
|
||||
|
|
|
@ -0,0 +1,294 @@
|
|||
# (c) Copyright 2015 Hewlett Packard Enterprise Development LP
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
HPE LeftHand SSH Client
|
||||
|
||||
.. module: ssh
|
||||
|
||||
:Author: Walter A. Boring IV
|
||||
:Description: This is the SSH Client that is used to make calls to
|
||||
the LeftHand where an existing REST API doesn't exist.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import paramiko
|
||||
from random import randint
|
||||
import re
|
||||
|
||||
from eventlet import greenthread
|
||||
from hpelefthandclient import exceptions
|
||||
|
||||
# Python 3+ override
|
||||
try:
|
||||
basestring
|
||||
python3 = False
|
||||
except NameError:
|
||||
basestring = str
|
||||
python3 = True
|
||||
|
||||
|
||||
class HPELeftHandSSHClient(object):
|
||||
"""This class is used to execute SSH commands on a LeftHand."""
|
||||
|
||||
log_debug = False
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.setLevel(logging.INFO)
|
||||
|
||||
def __init__(self, ip, login, password,
|
||||
port=16022, conn_timeout=None, privatekey=None,
|
||||
**kwargs):
|
||||
self.san_ip = ip
|
||||
self.san_ssh_port = port
|
||||
self.ssh_conn_timeout = conn_timeout
|
||||
self.san_login = login
|
||||
self.san_password = password
|
||||
self.san_private_key = privatekey
|
||||
|
||||
self._create_ssh(**kwargs)
|
||||
|
||||
def _create_ssh(self, **kwargs):
|
||||
try:
|
||||
ssh = paramiko.SSHClient()
|
||||
|
||||
known_hosts_file = kwargs.get('known_hosts_file', None)
|
||||
if known_hosts_file is None:
|
||||
ssh.load_system_host_keys()
|
||||
else:
|
||||
# Make sure we can open the file for appending first.
|
||||
# This is needed to create the file when we run CI tests with
|
||||
# no existing key file.
|
||||
open(known_hosts_file, 'a').close()
|
||||
ssh.load_host_keys(known_hosts_file)
|
||||
|
||||
missing_key_policy = kwargs.get('missing_key_policy', None)
|
||||
if missing_key_policy is None:
|
||||
missing_key_policy = paramiko.AutoAddPolicy()
|
||||
elif isinstance(missing_key_policy, basestring):
|
||||
# To make it configurable, allow string to be mapped to object.
|
||||
if missing_key_policy == paramiko.AutoAddPolicy().__class__.\
|
||||
__name__:
|
||||
missing_key_policy = paramiko.AutoAddPolicy()
|
||||
elif missing_key_policy == paramiko.RejectPolicy().__class__.\
|
||||
__name__:
|
||||
missing_key_policy = paramiko.RejectPolicy()
|
||||
elif missing_key_policy == paramiko.WarningPolicy().__class__.\
|
||||
__name__:
|
||||
missing_key_policy = paramiko.WarningPolicy()
|
||||
else:
|
||||
raise exceptions.SSHException(
|
||||
"Invalid missing_key_policy: %s" % missing_key_policy
|
||||
)
|
||||
|
||||
ssh.set_missing_host_key_policy(missing_key_policy)
|
||||
|
||||
self.ssh = ssh
|
||||
except Exception as e:
|
||||
msg = "Error connecting via ssh: %s" % e
|
||||
self._logger.error(msg)
|
||||
raise paramiko.SSHException(msg)
|
||||
|
||||
def _connect(self, ssh):
|
||||
if self.san_password:
|
||||
ssh.connect(self.san_ip,
|
||||
port=self.san_ssh_port,
|
||||
username=self.san_login,
|
||||
password=self.san_password,
|
||||
timeout=self.ssh_conn_timeout)
|
||||
elif self.san_privatekey:
|
||||
pkfile = os.path.expanduser(self.san_privatekey)
|
||||
privatekey = paramiko.RSAKey.from_private_key_file(pkfile)
|
||||
ssh.connect(self.san_ip,
|
||||
port=self.san_ssh_port,
|
||||
username=self.san_login,
|
||||
pkey=privatekey,
|
||||
timeout=self.ssh_conn_timeout)
|
||||
else:
|
||||
msg = "Specify a password or private_key"
|
||||
raise exceptions.SSHException(msg)
|
||||
|
||||
def open(self):
|
||||
"""Opens a new SSH connection if the transport layer is missing.
|
||||
|
||||
This can be called if an active SSH connection is open already.
|
||||
|
||||
"""
|
||||
# Create a new SSH connection if the transport layer is missing.
|
||||
if self.ssh:
|
||||
transport_active = False
|
||||
if self.ssh.get_transport():
|
||||
transport_active = self.ssh.get_transport().is_active()
|
||||
if not transport_active:
|
||||
try:
|
||||
self._connect(self.ssh)
|
||||
except Exception as e:
|
||||
msg = "Error connecting via ssh: %s" % e
|
||||
self._logger.error(msg)
|
||||
raise paramiko.SSHException(msg)
|
||||
|
||||
def close(self):
|
||||
if self.ssh:
|
||||
self.ssh.close()
|
||||
|
||||
def set_debug_flag(self, flag):
|
||||
"""
|
||||
This turns on ssh debugging output to console
|
||||
|
||||
:param flag: Set to True to enable debugging output
|
||||
:type flag: bool
|
||||
|
||||
"""
|
||||
if not HPELeftHandSSHClient.log_debug and flag:
|
||||
ch = logging.StreamHandler()
|
||||
self._logger.setLevel(logging.DEBUG)
|
||||
self._logger.addHandler(ch)
|
||||
HPELeftHandSSHClient.log_debug = True
|
||||
|
||||
def run(self, cmd):
|
||||
"""Runs a CLI command over SSH, without doing any result parsing."""
|
||||
self._logger.debug("SSH CMD = %s " % cmd)
|
||||
|
||||
(stdout, stderr) = self._run_ssh(cmd, False)
|
||||
# we have to strip out the input and exit lines
|
||||
if python3:
|
||||
tmp = stdout.decode().split("\r\n")
|
||||
else:
|
||||
tmp = stdout.split("\r\n")
|
||||
|
||||
out = tmp[8:len(tmp) - 3]
|
||||
self._logger.debug("OUT = %s" % out)
|
||||
return out
|
||||
|
||||
def _ssh_execute(self, cmd, check_exit_code=True):
|
||||
"""Executes the command via SSH."""
|
||||
self._logger.debug('Running cmd (SSH): %s', cmd)
|
||||
|
||||
channel = self.ssh.invoke_shell()
|
||||
stdin_stream = channel.makefile('wb')
|
||||
stdout_stream = channel.makefile('rb')
|
||||
stderr_stream = channel.makefile('rb')
|
||||
|
||||
stdin_stream.write('''%s
|
||||
exit
|
||||
''' % cmd)
|
||||
|
||||
# stdin.write('process_input would go here')
|
||||
# stdin.flush()
|
||||
|
||||
# NOTE(justinsb): This seems suspicious...
|
||||
# ...other SSH clients have buffering issues with this approach
|
||||
stdout = stdout_stream.read()
|
||||
stderr = stderr_stream.read()
|
||||
stdin_stream.close()
|
||||
stdout_stream.close()
|
||||
stderr_stream.close()
|
||||
|
||||
exit_status = channel.recv_exit_status()
|
||||
|
||||
# exit_status == -1 if no exit code was returned
|
||||
if exit_status != -1:
|
||||
self._logger.debug('Result was %s' % exit_status)
|
||||
if check_exit_code and exit_status != 0:
|
||||
msg = "command %s failed" % cmd
|
||||
self._logger.error(msg)
|
||||
raise exceptions.ProcessExecutionError(exit_code=exit_status,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cmd=cmd)
|
||||
channel.close()
|
||||
return (stdout, stderr)
|
||||
|
||||
def _run_ssh(self, cmd_list, check_exit=True, attempts=2):
|
||||
self.check_ssh_injection(cmd_list)
|
||||
command = ' '. join(cmd_list)
|
||||
|
||||
try:
|
||||
total_attempts = attempts
|
||||
while attempts > 0:
|
||||
attempts -= 1
|
||||
try:
|
||||
return self._ssh_execute(command,
|
||||
check_exit_code=check_exit)
|
||||
except Exception as e:
|
||||
self._logger.error(e)
|
||||
if attempts > 0:
|
||||
greenthread.sleep(randint(20, 500) / 100.0)
|
||||
if not self.ssh.get_transport().is_alive():
|
||||
self._create_ssh()
|
||||
|
||||
msg = ("SSH Command failed after '%(total_attempts)r' "
|
||||
"attempts : '%(command)s'" %
|
||||
{'total_attempts': total_attempts, 'command': command})
|
||||
self._logger.error(msg)
|
||||
raise exceptions.SSHException(message=msg)
|
||||
except Exception:
|
||||
self._logger.error("Error running ssh command: %s" % command)
|
||||
raise
|
||||
|
||||
def check_ssh_injection(self, cmd_list):
|
||||
ssh_injection_pattern = ['`', '$', '|', '||', ';', '&', '&&',
|
||||
'>', '>>', '<']
|
||||
|
||||
# Check whether injection attacks exist
|
||||
for arg in cmd_list:
|
||||
arg = arg.strip()
|
||||
|
||||
# Check for matching quotes on the ends
|
||||
is_quoted = re.match('^(?P<quote>[\'"])(?P<quoted>.*)(?P=quote)$',
|
||||
arg)
|
||||
if is_quoted:
|
||||
# Check for unescaped quotes within the quoted argument
|
||||
quoted = is_quoted.group('quoted')
|
||||
if quoted:
|
||||
if (re.match('[\'"]', quoted) or
|
||||
re.search('[^\\\\][\'"]', quoted)):
|
||||
raise exceptions.SSHInjectionThreat(
|
||||
command=str(cmd_list))
|
||||
else:
|
||||
# We only allow spaces within quoted arguments, and that
|
||||
# is the only special character allowed within quotes
|
||||
if len(arg.split()) > 1:
|
||||
raise exceptions.SSHInjectionThreat(command=str(cmd_list))
|
||||
|
||||
# Second, check whether danger character in command. So the shell
|
||||
# special operator must be a single argument.
|
||||
for c in ssh_injection_pattern:
|
||||
if arg == c:
|
||||
continue
|
||||
|
||||
result = arg.find(c)
|
||||
if not result == -1:
|
||||
if result == 0 or not arg[result - 1] == '\\':
|
||||
raise exceptions.SSHInjectionThreat(command=cmd_list)
|
||||
|
||||
def was_command_successful(self, output):
|
||||
"""
|
||||
Given the entire output of an SSH command, this will check to see if
|
||||
the result returned is 0, aka it was successful. If result is not 0,
|
||||
the command failed and we return False.
|
||||
|
||||
:param output: The output string of an SSH command
|
||||
:type output: str
|
||||
|
||||
:returns: True if the command was successful
|
||||
"""
|
||||
output_string = ''.join(output)
|
||||
match = re.match('.*result\s+0', output_string)
|
||||
if not match:
|
||||
raise exceptions.SSHException(
|
||||
"The command did not execute successfully.")
|
||||
|
||||
return True
|
|
@ -1 +1,3 @@
|
|||
eventlet
|
||||
paramiko>=1.13.0
|
||||
requests
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
eventlet
|
||||
paramiko>=1.13.0
|
||||
requests
|
||||
|
|
6
setup.py
6
setup.py
|
@ -15,9 +15,9 @@ setup(
|
|||
author_email="kurt.f.martin@hpe.com",
|
||||
maintainer="Kurt Martin",
|
||||
keywords=["hpe", "lefthand", "storevirtual", "rest"],
|
||||
requires=['requests'],
|
||||
install_requires=['requests'],
|
||||
tests_require=["nose", "nose-testconfig", "flask", "Werkzeug", "flake8"],
|
||||
requires=['paramiko', 'eventlet', 'requests'],
|
||||
install_requires=['paramiko', 'eventlet', 'requests'],
|
||||
tests_require=["mock", "nose", "nose-testconfig", "flask", "Werkzeug", "flake8"],
|
||||
license="Apache License, Version 2.0",
|
||||
packages=find_packages(),
|
||||
provides=['hplefthandclient'],
|
||||
|
|
|
@ -2,7 +2,7 @@ nose
|
|||
nose-testconfig
|
||||
flask
|
||||
Werkzeug
|
||||
|
||||
mock
|
||||
flake8 # Used by tox
|
||||
coverage
|
||||
sphinx
|
||||
|
|
|
@ -2,7 +2,7 @@ nose
|
|||
nose-testconfig
|
||||
flask
|
||||
Werkzeug
|
||||
|
||||
mock
|
||||
flake8 # Used by tox
|
||||
coverage
|
||||
sphinx
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
# (c) Copyright 2015 Hewlett Packard Enterprise Development LP
|
||||
#
|
||||
# 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.
|
||||
""" Test SSH server."""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
paramiko.util.log_to_file('paramiko_server.log')
|
||||
|
||||
|
||||
class CliParseException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CliArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def error(self, message):
|
||||
usage = super(CliArgumentParser, self).format_help()
|
||||
full_message = "%s\r\n%s" % (message, usage)
|
||||
raise CliParseException(full_message)
|
||||
|
||||
def parse_args(self, *args):
|
||||
return super(CliArgumentParser, self).parse_args(args[1:])
|
||||
|
||||
|
||||
class Cli(object):
|
||||
|
||||
def __init__(self):
|
||||
self.log_name = 'paramiko.LeftHandCLI'
|
||||
self.logger = paramiko.util.get_logger(self.log_name)
|
||||
|
||||
self.fpgs = {}
|
||||
self.vfss = {}
|
||||
|
||||
def do_cli_other(self, *args):
|
||||
msg = 'FAIL! Mock SSH CLI does not know how to "%s".' % ' '.join(args)
|
||||
self.logger.log(logging.ERROR, msg)
|
||||
return msg
|
||||
|
||||
def do_cli_exit(self, *args):
|
||||
self.logger.log(logging.INFO, "quiting... g'bye")
|
||||
return ''
|
||||
|
||||
def do_cli_quit(self, *args):
|
||||
self.logger.log(logging.INFO, "quiting... g'bye")
|
||||
return ''
|
||||
|
||||
def process_command(self, cmd):
|
||||
self.logger.log(logging.INFO, cmd)
|
||||
if cmd is None:
|
||||
print("returnNone")
|
||||
return ''
|
||||
args = shlex.split(cmd)
|
||||
if args:
|
||||
method = getattr(self, 'do_cli_' + args[0], self.do_cli_other)
|
||||
try:
|
||||
return method(*args)
|
||||
except Exception as cmd_exception:
|
||||
return str(cmd_exception)
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
class ParamikoServer(paramiko.ServerInterface):
|
||||
|
||||
def __init__(self):
|
||||
self.event = threading.Event()
|
||||
|
||||
def check_channel_request(self, kind, chanid):
|
||||
return paramiko.OPEN_SUCCEEDED
|
||||
|
||||
def check_auth_none(self, username):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
|
||||
def check_auth_password(self, username, password):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
|
||||
def check_auth_publickey(self, username, key):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
return 'password,publickey,none'
|
||||
|
||||
def check_channel_shell_request(self, c):
|
||||
self.event.set()
|
||||
return True
|
||||
|
||||
def check_channel_pty_request(self, c, term, width, height, pixelwidth,
|
||||
pixelheight, modes):
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
port = int(sys.argv[1])
|
||||
else:
|
||||
port = 2200
|
||||
|
||||
key_file = os.path.expanduser('~/.ssh/id_rsa')
|
||||
host_key = paramiko.RSAKey(filename=key_file)
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(('', int(port)))
|
||||
s.listen(60)
|
||||
print("Listening for SSH client connections...")
|
||||
connection, address = s.accept()
|
||||
transport = None
|
||||
channel = None
|
||||
try:
|
||||
transport = paramiko.Transport(connection)
|
||||
transport.load_server_moduli()
|
||||
transport.add_server_key(host_key)
|
||||
server = ParamikoServer()
|
||||
transport.start_server(server=server)
|
||||
|
||||
cliProcessor = Cli()
|
||||
|
||||
while True:
|
||||
channel = transport.accept(60)
|
||||
if channel is None:
|
||||
print("Failed to get SSH channel.")
|
||||
sys.exit(1)
|
||||
|
||||
print("Connected")
|
||||
server.event.wait(10)
|
||||
|
||||
if not server.event.isSet():
|
||||
print("No shell set")
|
||||
sys.exit(1)
|
||||
|
||||
fio = channel.makefile('rU')
|
||||
commands = []
|
||||
command = None
|
||||
while not (command == 'exit' or command == 'quit'):
|
||||
command = fio.readline().strip('\r\n')
|
||||
commands.append(command)
|
||||
|
||||
to_send = '\r\n'.join(commands)
|
||||
channel.send(to_send)
|
||||
|
||||
output = ['']
|
||||
prompt = "FAKE-LeftHand-CLI cli% "
|
||||
for cmd in commands:
|
||||
output.append('%s%s' % (prompt, cmd))
|
||||
result = cliProcessor.process_command(cmd)
|
||||
if result is not None:
|
||||
output.append(result)
|
||||
output_to_send = '\r\n'.join(output)
|
||||
channel.send(output_to_send)
|
||||
channel.close()
|
||||
print("Disconnected")
|
||||
|
||||
finally:
|
||||
if channel:
|
||||
channel.close()
|
||||
if transport:
|
||||
try:
|
||||
transport.close()
|
||||
print("transport closed")
|
||||
except Exception as e:
|
||||
print("transport close exception %s" % e)
|
||||
pass
|
|
@ -0,0 +1,212 @@
|
|||
# (c) Copyright 2015 Hewlett Packard Enterprise Development LP
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock
|
||||
import paramiko
|
||||
import unittest
|
||||
|
||||
import test_HPELeftHandClient_base
|
||||
from hpelefthandclient import exceptions
|
||||
from hpelefthandclient import ssh
|
||||
|
||||
# Python 3+ override
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
user = "u"
|
||||
password = "p"
|
||||
ip = "10.10.22.241"
|
||||
api_url = "http://10.10.22.241:8008/api/v1"
|
||||
|
||||
|
||||
class HPELeftHandClientMockSSHTestCase(test_HPELeftHandClient_base
|
||||
.HPELeftHandClientBaseTestCase):
|
||||
|
||||
def mock_paramiko(self, known_hosts_file, missing_key_policy):
|
||||
"""Verify that these params get into paramiko."""
|
||||
|
||||
mock_lhk = mock.Mock()
|
||||
mock_lshk = mock.Mock()
|
||||
mock_smhkp = mock.Mock()
|
||||
mock_smhkp.side_effect = Exception("Let's end this here")
|
||||
|
||||
with mock.patch('paramiko.client.SSHClient.load_system_host_keys',
|
||||
mock_lshk, create=True):
|
||||
with mock.patch('paramiko.client.SSHClient.load_host_keys',
|
||||
mock_lhk, create=True):
|
||||
with mock.patch('paramiko.client.SSHClient.'
|
||||
'set_missing_host_key_policy',
|
||||
mock_smhkp, create=True):
|
||||
try:
|
||||
self.cl.setSSHOptions(
|
||||
ip, user, password,
|
||||
known_hosts_file=known_hosts_file,
|
||||
missing_key_policy=missing_key_policy)
|
||||
except paramiko.SSHException as e:
|
||||
if 'Invalid missing_key_policy' in str(e):
|
||||
raise e
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if known_hosts_file is None:
|
||||
mock_lhk.assert_not_called()
|
||||
mock_lshk.assert_called_with()
|
||||
else:
|
||||
mock_lhk.assert_called_with(known_hosts_file)
|
||||
mock_lshk.assert_not_called()
|
||||
|
||||
actual = mock_smhkp.call_args[0][0].__class__.__name__
|
||||
if missing_key_policy is None:
|
||||
# If missing, it should be called with our
|
||||
# default which is an AutoAddPolicy
|
||||
expected = paramiko.AutoAddPolicy().__class__.__name__
|
||||
elif isinstance(missing_key_policy, basestring):
|
||||
expected = missing_key_policy
|
||||
else:
|
||||
expected = missing_key_policy.__class__.__name__
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def do_mock_create_ssh(self, known_hosts_file, missing_key_policy):
|
||||
"""Verify that params are getting forwarded to _create_ssh()."""
|
||||
|
||||
mock_ssh = mock.Mock()
|
||||
path = 'hpelefthandclient.ssh.HPELeftHandSSHClient._create_ssh'
|
||||
with mock.patch(path, mock_ssh, create=True):
|
||||
|
||||
self.cl.setSSHOptions(ip, user, password,
|
||||
known_hosts_file=known_hosts_file,
|
||||
missing_key_policy=missing_key_policy)
|
||||
|
||||
mock_ssh.assert_called_with(missing_key_policy=missing_key_policy,
|
||||
known_hosts_file=known_hosts_file)
|
||||
|
||||
# Create a mocked ssh object for the client so that it can be
|
||||
# "closed" during a logout.
|
||||
self.cl.ssh = mock.MagicMock()
|
||||
|
||||
@mock.patch('hpelefthandclient.ssh.HPELeftHandSSHClient')
|
||||
def do_mock_ssh(self, known_hosts_file, missing_key_policy,
|
||||
mock_ssh_client):
|
||||
"""Verify that params are getting forwarded to HPELeftHandSSHClient."""
|
||||
|
||||
self.cl.setSSHOptions(ip, user, password,
|
||||
known_hosts_file=known_hosts_file,
|
||||
missing_key_policy=missing_key_policy)
|
||||
|
||||
mock_ssh_client.assert_called_with(
|
||||
ip, user, password, 16022, None, None,
|
||||
missing_key_policy=missing_key_policy,
|
||||
known_hosts_file=known_hosts_file)
|
||||
|
||||
def base(self, known_hosts_file, missing_key_policy):
|
||||
self.printHeader("%s : known_hosts_file=%s missing_key_policy=%s" %
|
||||
(unittest.TestCase.id(self),
|
||||
known_hosts_file, missing_key_policy))
|
||||
self.do_mock_ssh(known_hosts_file, missing_key_policy)
|
||||
self.do_mock_create_ssh(known_hosts_file, missing_key_policy)
|
||||
self.mock_paramiko(known_hosts_file, missing_key_policy)
|
||||
self.printFooter(unittest.TestCase.id(self))
|
||||
|
||||
def test_auto_add_policy(self):
|
||||
known_hosts_file = "test_bogus_known_hosts_file"
|
||||
missing_key_policy = "AutoAddPolicy"
|
||||
self.base(known_hosts_file, missing_key_policy)
|
||||
|
||||
def test_warning_policy(self):
|
||||
known_hosts_file = "test_bogus_known_hosts_file"
|
||||
missing_key_policy = "WarningPolicy"
|
||||
self.base(known_hosts_file, missing_key_policy)
|
||||
|
||||
def test_reject_policy(self):
|
||||
known_hosts_file = "test_bogus_known_hosts_file"
|
||||
missing_key_policy = "RejectPolicy"
|
||||
self.base(known_hosts_file, missing_key_policy)
|
||||
|
||||
def test_known_hosts_file_is_none(self):
|
||||
known_hosts_file = None
|
||||
missing_key_policy = paramiko.RejectPolicy()
|
||||
self.base(known_hosts_file, missing_key_policy)
|
||||
|
||||
def test_both_settings_are_none(self):
|
||||
known_hosts_file = None
|
||||
missing_key_policy = None
|
||||
self.base(known_hosts_file, missing_key_policy)
|
||||
|
||||
def test_bogus_missing_key_policy(self):
|
||||
known_hosts_file = None
|
||||
missing_key_policy = "bogus"
|
||||
self.assertRaises(paramiko.SSHException,
|
||||
self.base,
|
||||
known_hosts_file,
|
||||
missing_key_policy)
|
||||
|
||||
def test_create_ssh_except(self):
|
||||
"""Make sure that SSH exceptions are not quietly eaten."""
|
||||
|
||||
self.cl.setSSHOptions(ip,
|
||||
user,
|
||||
password,
|
||||
known_hosts_file=None,
|
||||
missing_key_policy=paramiko.AutoAddPolicy)
|
||||
|
||||
self.cl.ssh.ssh = mock.Mock()
|
||||
self.cl.ssh.ssh.invoke_shell.side_effect = Exception('boom')
|
||||
|
||||
cmd = ['fake']
|
||||
self.assertRaises(exceptions.SSHException, self.cl.ssh._run_ssh, cmd)
|
||||
|
||||
self.cl.ssh.ssh.assert_has_calls(
|
||||
[
|
||||
mock.call.get_transport(),
|
||||
mock.call.get_transport().is_alive(),
|
||||
mock.call.invoke_shell(),
|
||||
mock.call.get_transport(),
|
||||
mock.call.get_transport().is_alive(),
|
||||
]
|
||||
)
|
||||
|
||||
def test_was_command_successful_true(self):
|
||||
cmd_out = ['',
|
||||
'HP StoreVirtual LeftHand OS Command Line Interface',
|
||||
'(C) Copyright 2007-2013',
|
||||
'',
|
||||
'RESPONSE',
|
||||
' result 0']
|
||||
ssh_client = ssh.HPELeftHandSSHClient('foo', 'bar', 'biz')
|
||||
out = ssh_client.was_command_successful(cmd_out)
|
||||
self.assertTrue(out)
|
||||
|
||||
def test_was_command_successful_false(self):
|
||||
# Create our fake SSH client
|
||||
ssh_client = ssh.HPELeftHandSSHClient('foo', 'bar', 'biz')
|
||||
|
||||
# Test invalid command output
|
||||
cmd_out = ['invalid']
|
||||
self.assertRaises(exceptions.SSHException,
|
||||
ssh_client.was_command_successful,
|
||||
cmd_out)
|
||||
|
||||
# Test valid command output, but command failed
|
||||
cmd_out = ['',
|
||||
'HP StoreVirtual LeftHand OS Command Line Interface',
|
||||
'(C) Copyright 2007-2013',
|
||||
'',
|
||||
'RESPONSE',
|
||||
' result 8080878378']
|
||||
self.assertRaises(exceptions.SSHException,
|
||||
ssh_client.was_command_successful,
|
||||
cmd_out)
|
|
@ -52,7 +52,30 @@ class HPELeftHandClientBaseTestCase(unittest.TestCase):
|
|||
unitTest = config['TEST']['unit'].lower() == 'true'
|
||||
startFlask = config['TEST']['start_flask_server'].lower() == 'true'
|
||||
|
||||
def setUp(self):
|
||||
ssh_port = None
|
||||
if 'ssh_port' in config['TEST']:
|
||||
ssh_port = int(config['TEST']['ssh_port'])
|
||||
elif unitTest:
|
||||
ssh_port = 2200
|
||||
else:
|
||||
ssh_port = 16022
|
||||
|
||||
# Don't setup SSH unless needed. It slows things down.
|
||||
withSSH = False
|
||||
|
||||
if 'known_hosts_file' in config['TEST']:
|
||||
known_hosts_file = config['TEST']['known_hosts_file']
|
||||
else:
|
||||
known_hosts_file = None
|
||||
|
||||
if 'missing_key_policy' in config['TEST']:
|
||||
missing_key_policy = config['TEST']['missing_key_policy']
|
||||
else:
|
||||
missing_key_policy = None
|
||||
|
||||
def setUp(self, withSSH=False):
|
||||
|
||||
self.withSSH = withSSH
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(
|
||||
inspect.getfile(inspect.currentframe())))
|
||||
|
@ -82,10 +105,50 @@ class HPELeftHandClientBaseTestCase(unittest.TestCase):
|
|||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
if self.withSSH:
|
||||
self.printHeader('Using paramiko SSH server on port %s' %
|
||||
self.ssh_port)
|
||||
|
||||
ssh_script = 'HPELeftHandMockServer_ssh.py'
|
||||
ssh_path = "%s/%s" % (cwd, ssh_script)
|
||||
|
||||
self.mockSshServer = subprocess.Popen([sys.executable,
|
||||
ssh_path,
|
||||
str(self.ssh_port)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE)
|
||||
time.sleep(1)
|
||||
else:
|
||||
self.printHeader('Using LeftHand ' + self.url_lhos)
|
||||
self.cl = client.HPELeftHandClient(self.url_lhos)
|
||||
|
||||
if self.withSSH:
|
||||
# This seems to slow down the test cases, so only use this when
|
||||
# requested
|
||||
if self.unitTest:
|
||||
# The mock SSH server can be accessed at 0.0.0.0.
|
||||
ip = '0.0.0.0'
|
||||
else:
|
||||
parsed_lh_url = urlparse(self.url_lhos)
|
||||
ip = parsed_lh_url.hostname.split(':').pop()
|
||||
try:
|
||||
# Now that we don't do keep-alive, the conn_timeout needs to
|
||||
# be set high enough to avoid sometimes slow response in
|
||||
# the File Persona tests.
|
||||
self.cl.setSSHOptions(
|
||||
ip,
|
||||
self.user,
|
||||
self.password,
|
||||
port=self.ssh_port,
|
||||
conn_timeout=500,
|
||||
known_hosts_file=self.known_hosts_file,
|
||||
missing_key_policy=self.missing_key_policy)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
self.fail("failed to start ssh client")
|
||||
|
||||
if self.debug:
|
||||
self.cl.debug_rest(True)
|
||||
|
||||
|
@ -97,6 +160,8 @@ class HPELeftHandClientBaseTestCase(unittest.TestCase):
|
|||
#TODO: it seems to kill all the process except the last one...
|
||||
#don't know why
|
||||
self.mockServer.kill()
|
||||
if self.withSSH:
|
||||
self.mockSshServer.kill()
|
||||
|
||||
def printHeader(self, name):
|
||||
print("\n##Start testing '%s'" % name)
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
"""Test class of LeftHand Client handling volumes & snapshots """
|
||||
|
||||
from testconfig import config
|
||||
|
||||
import test_HPELeftHandClient_base
|
||||
|
||||
from hpelefthandclient import exceptions
|
||||
|
@ -25,6 +27,10 @@ SNAP_NAME1 = 'SNAP_UNIT_TEST1_' + test_HPELeftHandClient_base.TIME
|
|||
SNAP_NAME2 = 'SNAP_UNIT_TEST2_' + test_HPELeftHandClient_base.TIME
|
||||
|
||||
|
||||
def is_live_test():
|
||||
return config['TEST']['unit'].lower() == 'false'
|
||||
|
||||
|
||||
class HPELeftHandClientVolumeTestCase(test_HPELeftHandClient_base.
|
||||
HPELeftHandClientBaseTestCase):
|
||||
|
||||
|
@ -32,7 +38,10 @@ class HPELeftHandClientVolumeTestCase(test_HPELeftHandClient_base.
|
|||
MIN_CG_API_VERSION = '1.2'
|
||||
|
||||
def setUp(self):
|
||||
super(HPELeftHandClientVolumeTestCase, self).setUp()
|
||||
ssh = False
|
||||
if is_live_test():
|
||||
ssh = True
|
||||
super(HPELeftHandClientVolumeTestCase, self).setUp(withSSH=ssh)
|
||||
|
||||
try:
|
||||
cluster_info = self.cl.getClusterByName(
|
||||
|
|
Loading…
Reference in New Issue