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:
Alex O'Rourke 2015-11-17 14:11:31 -08:00
parent e8a8478c1b
commit 84de6562e1
15 changed files with 873 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

294
hpelefthandclient/ssh.py Normal file
View File

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

View File

@ -1 +1,3 @@
eventlet
paramiko>=1.13.0
requests

View File

@ -1 +1,3 @@
eventlet
paramiko>=1.13.0
requests

View File

@ -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'],

View File

@ -2,7 +2,7 @@ nose
nose-testconfig
flask
Werkzeug
mock
flake8 # Used by tox
coverage
sphinx

View File

@ -2,7 +2,7 @@ nose
nose-testconfig
flask
Werkzeug
mock
flake8 # Used by tox
coverage
sphinx

183
test/HPELeftHandMockServer_ssh.py Executable file
View File

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

View File

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

View File

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

View File

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