Merge tag '0.5.0' into debian/kilo

Release 0.5.0

This tempest-lib release includes:
 * Support for specifying a prefix on the rand_name() function
 * Migration of the ssh module from tempest
 * A few cleanups and missing pieces for the auth.py module
 * subunit-trace bug fix
 * Improved unit tests and docstring coverage

This also marks the last release which will include subunit-trace as
part of tempest-lib. This will be moved to the os-testr project in
future releases. (and os-testr will be added as a requirement)
This commit is contained in:
James Page
2015-06-01 10:50:52 +01:00
17 changed files with 861 additions and 42 deletions

View File

@@ -7,5 +7,6 @@ fixtures>=0.3.14
iso8601>=0.1.9
jsonschema>=2.0.0,<3.0.0
httplib2>=0.7.5
paramiko>=1.13.0
six>=1.9.0
oslo.log>=0.4.0 # Apache-2.0
oslo.log>=1.0.0 # Apache-2.0

View File

@@ -17,13 +17,10 @@ import abc
import copy
import datetime
import re
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
from oslo_log import log as logging
import six
from six.moves.urllib import parse as urlparse
from tempest_lib import exceptions
from tempest_lib.services.identity.v2 import token_client as json_v2id

View File

@@ -190,7 +190,11 @@ def run_time():
runtime = 0.0
for k, v in RESULTS.items():
for test in v:
runtime += float(get_duration(test['timestamps']).strip('s'))
test_dur = get_duration(test['timestamps']).strip('s')
# NOTE(toabctl): get_duration() can return an empty string
# which leads to a ValueError when casting to float
if test_dur:
runtime += float(test_dur)
return runtime

View File

@@ -270,8 +270,8 @@ class RestClient(object):
"""Send a HTTP DELETE request using keystone service catalog and auth
:param str url: the relative url to send the post request to
:param dict body: the request body
:param dict headers: The headers to use for the request
:param dict body: the request body
:param dict extra_headers: If the headers returned by the get_headers()
method are to be used but additional headers
are needed in the request pass them in as a
@@ -554,16 +554,16 @@ class RestClient(object):
headers and the catalog to determine the endpoint to use for the
baseurl to send the request to. Additionally
When a response is recieved it will check it to see if an error
response was recieved. If it was an exception will be raised to enable
When a response is received it will check it to see if an error
response was received. If it was an exception will be raised to enable
it to be handled quickly.
This method will also handle rate-limiting, if a 413 response code is
recieved it will retry the request after waiting the 'retry-after'
received it will retry the request after waiting the 'retry-after'
duration from the header.
:param str url: Relative url to send the request to
:param str method: The HTTP verb to use for the request
:param str url: Relative url to send the request to
:param dict extra_headers: If specified without the headers kwarg the
headers sent with the request will be the
combination from the get_headers() method
@@ -578,23 +578,23 @@ class RestClient(object):
and the second the response body
:raises InvalidContentType: If the content-type of the response isn't
an expect type or a 415 response code is
recieved
:raises Unauthorized: If a 401 response code is recieved
:raises Forbidden: If a 403 response code is recieved
:raises NotFound: If a 404 response code is recieved
:raises BadRequest: If a 400 response code is recieved
:raises Conflict: If a 409 response code is recieved
:raies Overlimit: If a 413 response code is recieved and over_limit is
received
:raises Unauthorized: If a 401 response code is received
:raises Forbidden: If a 403 response code is received
:raises NotFound: If a 404 response code is received
:raises BadRequest: If a 400 response code is received
:raises Conflict: If a 409 response code is received
:raises OverLimit: If a 413 response code is received and over_limit is
not in the response body
:raises RateLimitExceeded: If a 413 response code is recieved and
:raises RateLimitExceeded: If a 413 response code is received and
over_limit is in the response body
:raises UnprocessableEntity: If a 422 response code is recieved
:raises UnprocessableEntity: If a 422 response code is received
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
and couldn't be parsed
:raises NotImplemented: If a 501 response code is recieved
:raises ServerFault: If a 500 response code is recieved
:raises NotImplemented: If a 501 response code is received
:raises ServerFault: If a 500 response code is received
:raises UnexpectedResponseCode: If a response code above 400 is
recieved and it doesn't fall into any
received and it doesn't fall into any
of the handled checks
"""
# if extra_headers is True

152
tempest_lib/common/ssh.py Normal file
View File

@@ -0,0 +1,152 @@
# Copyright 2012 OpenStack Foundation
# 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 select
import socket
import time
import warnings
from oslo_log import log as logging
import six
from tempest_lib import exceptions
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import paramiko
LOG = logging.getLogger(__name__)
class Client(object):
def __init__(self, host, username, password=None, timeout=300, pkey=None,
channel_timeout=10, look_for_keys=False, key_filename=None):
self.host = host
self.username = username
self.password = password
if isinstance(pkey, six.string_types):
pkey = paramiko.RSAKey.from_private_key(
six.StringIO(str(pkey)))
self.pkey = pkey
self.look_for_keys = look_for_keys
self.key_filename = key_filename
self.timeout = int(timeout)
self.channel_timeout = float(channel_timeout)
self.buf_size = 1024
def _get_ssh_connection(self, sleep=1.5, backoff=1):
"""Returns an ssh connection to the specified host."""
bsleep = sleep
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(
paramiko.AutoAddPolicy())
_start_time = time.time()
if self.pkey is not None:
LOG.info("Creating ssh connection to '%s' as '%s'"
" with public key authentication",
self.host, self.username)
else:
LOG.info("Creating ssh connection to '%s' as '%s'"
" with password %s",
self.host, self.username, str(self.password))
attempts = 0
while True:
try:
ssh.connect(self.host, username=self.username,
password=self.password,
look_for_keys=self.look_for_keys,
key_filename=self.key_filename,
timeout=self.channel_timeout, pkey=self.pkey)
LOG.info("ssh connection to %s@%s successfuly created",
self.username, self.host)
return ssh
except (socket.error,
paramiko.SSHException) as e:
if self._is_timed_out(_start_time):
LOG.exception("Failed to establish authenticated ssh"
" connection to %s@%s after %d attempts",
self.username, self.host, attempts)
raise exceptions.SSHTimeout(host=self.host,
user=self.username,
password=self.password)
bsleep += backoff
attempts += 1
LOG.warning("Failed to establish authenticated ssh"
" connection to %s@%s (%s). Number attempts: %s."
" Retry after %d seconds.",
self.username, self.host, e, attempts, bsleep)
time.sleep(bsleep)
def _is_timed_out(self, start_time):
return (time.time() - self.timeout) > start_time
def exec_command(self, cmd):
"""Execute the specified command on the server
Note that this method is reading whole command outputs to memory, thus
shouldn't be used for large outputs.
:param str cmd: Command to run at remote server.
:returns: data read from standard output of the command.
:raises: SSHExecCommandFailed if command returns nonzero
status. The exception contains command status stderr content.
:raises: TimeoutException if cmd doesn't end when timeout expires.
"""
ssh = self._get_ssh_connection()
transport = ssh.get_transport()
channel = transport.open_session()
channel.fileno() # Register event pipe
channel.exec_command(cmd)
channel.shutdown_write()
out_data = []
err_data = []
poll = select.poll()
poll.register(channel, select.POLLIN)
start_time = time.time()
while True:
ready = poll.poll(self.channel_timeout)
if not any(ready):
if not self._is_timed_out(start_time):
continue
raise exceptions.TimeoutException(
"Command: '{0}' executed on host '{1}'.".format(
cmd, self.host))
if not ready[0]: # If there is nothing to read.
continue
out_chunk = err_chunk = None
if channel.recv_ready():
out_chunk = channel.recv(self.buf_size)
out_data += out_chunk,
if channel.recv_stderr_ready():
err_chunk = channel.recv_stderr(self.buf_size)
err_data += err_chunk,
if channel.closed and not err_chunk and not out_chunk:
break
exit_status = channel.recv_exit_status()
if 0 != exit_status:
raise exceptions.SSHExecCommandFailed(
command=cmd, exit_status=exit_status,
strerror=''.join(err_data))
return ''.join(out_data)
def test_connection_auth(self):
"""Raises an exception when we can not connect to server via ssh."""
connection = self._get_ssh_connection()
connection.close()

View File

@@ -20,32 +20,70 @@ import uuid
def rand_uuid():
"""Generate a random UUID string
:return: a random UUID (e.g. '1dc12c7d-60eb-4b61-a7a2-17cf210155b6')
:rtype: string
"""
return str(uuid.uuid4())
def rand_uuid_hex():
"""Generate a random UUID hex string
:return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c')
:rtype: string
"""
return uuid.uuid4().hex
def rand_name(name=''):
def rand_name(name='', prefix=None):
"""Generate a random name that inclues a random number
:param str name: The name that you want to include
:param str prefix: The prefix that you want to include
:return: a random name. The format is
'<prefix>-<random number>-<name>-<random number>'.
(e.g. 'prefixfoo-1308607012-namebar-154876201')
:rtype: string
"""
randbits = str(random.randint(1, 0x7fffffff))
rand_name = randbits
if name:
return name + '-' + randbits
else:
return randbits
rand_name = name + '-' + rand_name
if prefix:
rand_name = prefix + '-' + rand_name
return rand_name
def rand_url():
"""Generate a random url that inclues a random number
:return: a random url. The format is 'https://url-<random number>.com'.
(e.g. 'https://url-154876201.com')
:rtype: string
"""
randbits = str(random.randint(1, 0x7fffffff))
return 'https://url-' + randbits + '.com'
def rand_int_id(start=0, end=0x7fffffff):
"""Generate a random integer value
:param int start: The value that you expect to start here
:param int end: The value that you expect to end here
:return: a random integer value
:rtype: int
"""
return random.randint(start, end)
def rand_mac_address():
"""Generate an Ethernet MAC address."""
"""Generate an Ethernet MAC address
:return: an random Ethernet MAC address
:rtype: string
"""
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
# bridge mac addresses don't change, but it appears to
# conflict with libvirt, so we use the next highest octet
@@ -60,14 +98,27 @@ def rand_mac_address():
def parse_image_id(image_ref):
"""Return the image id from a given image ref."""
"""Return the image id from a given image ref
This function just returns the last word of the given image ref string
splitting with '/'.
:param str image_ref: a string that includes the image id
:return: the image id string
:rtype: string
"""
return image_ref.rsplit('/')[-1]
def arbitrary_string(size=4, base_text=None):
"""Return size characters from base_text
Repeating the base_text infinitely if needed.
This generates a string with an arbitrary number of characters, generated
by looping the base_text string. If the size is smaller than the size of
base_text, returning string is shrinked to the size.
:param int size: a returning charactors size
:param str base_text: a string you want to repeat
:return: size string
:rtype: string
"""
if not base_text:
base_text = 'test'
@@ -75,12 +126,24 @@ def arbitrary_string(size=4, base_text=None):
def random_bytes(size=1024):
"""Return size randomly selected bytes as a string."""
"""Return size randomly selected bytes as a string
:param int size: a returning bytes size
:return: size randomly bytes
:rtype: string
"""
return ''.join([chr(random.randint(0, 255))
for i in range(size)])
def get_ipv6_addr_by_EUI64(cidr, mac):
"""Generate a IPv6 addr by EUI-64 with CIDR and MAC
:param str cidr: a IPv6 CIDR
:param str mac: a MAC address
:return: an IPv6 Address
:rtype: netaddr.IPAddress
"""
# Check if the prefix is IPv4 address
is_ipv4 = netaddr.valid_ipv4(cidr)
if is_ipv4:

View File

@@ -150,4 +150,19 @@ class IdentityError(TempestException):
class EndpointNotFound(TempestException):
message = "Endpoint not found"
message = "Endpoint not found"
class InvalidCredentials(TempestException):
message = "Invalid Credentials"
class SSHTimeout(TempestException):
message = ("Connection to the %(host)s via SSH timed out.\n"
"User: %(user)s, Password: %(password)s")
class SSHExecCommandFailed(TempestException):
"""Raised when remotely executed command returns nonzero status."""
message = ("Command '%(command)s', exit status: %(exit_status)d, "
"Error:\n%(strerror)s")

View File

View File

@@ -0,0 +1,61 @@
# Copyright 2015 SUSE Linux GmbH
# 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.
from datetime import datetime as dt
from ddt import data
from ddt import ddt
from ddt import unpack
from mock import patch
from tempest_lib.cmd import subunit_trace
from tempest_lib.tests import base
@ddt
class TestSubunitTrace(base.TestCase):
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
dt(2015, 4, 17, 22, 23, 14, 111111)],
"0.000000s"),
([dt(2015, 4, 17, 22, 23, 14, 111111),
dt(2015, 4, 17, 22, 23, 15, 111111)],
"1.000000s"),
([dt(2015, 4, 17, 22, 23, 14, 111111),
None],
""))
@unpack
def test_get_durating(self, timestamps, expected_result):
self.assertEqual(subunit_trace.get_duration(timestamps),
expected_result)
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
dt(2015, 4, 17, 22, 23, 14, 111111)],
0.0),
([dt(2015, 4, 17, 22, 23, 14, 111111),
dt(2015, 4, 17, 22, 23, 15, 111111)],
1.0),
([dt(2015, 4, 17, 22, 23, 14, 111111),
None],
0.0))
@unpack
def test_run_time(self, timestamps, expected_result):
patched_res = {
0: [
{'timestamps': timestamps}
]
}
with patch.dict(subunit_trace.RESULTS, patched_res, clear=True):
self.assertEqual(subunit_trace.run_time(), expected_result)

View File

@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from tempest_lib.common.utils import data_utils
from tempest_lib.tests import base
@@ -48,6 +49,20 @@ class TestDataUtils(base.TestCase):
self.assertTrue(actual.startswith('foo'))
self.assertNotEqual(actual, actual2)
def test_rand_name_with_prefix(self):
actual = data_utils.rand_name(prefix='prefix-str')
self.assertIsInstance(actual, str)
self.assertRegexpMatches(actual, "^prefix-str-")
actual2 = data_utils.rand_name(prefix='prefix-str')
self.assertNotEqual(actual, actual2)
def test_rand_url(self):
actual = data_utils.rand_url()
self.assertIsInstance(actual, str)
self.assertRegexpMatches(actual, "^https://url-[0-9]*\.com$")
actual2 = data_utils.rand_url()
self.assertNotEqual(actual, actual2)
def test_rand_int(self):
actual = data_utils.rand_int_id()
self.assertIsInstance(actual, int)
@@ -75,3 +90,50 @@ class TestDataUtils(base.TestCase):
self.assertEqual(actual, "abc" * int(30 / len("abc")))
actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf")
self.assertEqual(actual, "deadb")
def test_random_bytes(self):
actual = data_utils.random_bytes() # default size=1024
self.assertIsInstance(actual, str)
self.assertRegexpMatches(actual, "^[\x00-\xFF]{1024}")
actual2 = data_utils.random_bytes()
self.assertNotEqual(actual, actual2)
actual = data_utils.random_bytes(size=2048)
self.assertRegexpMatches(actual, "^[\x00-\xFF]{2048}")
def test_get_ipv6_addr_by_EUI64(self):
actual = data_utils.get_ipv6_addr_by_EUI64('2001:db8::',
'00:16:3e:33:44:55')
self.assertIsInstance(actual, netaddr.IPAddress)
self.assertEqual(actual,
netaddr.IPAddress('2001:db8::216:3eff:fe33:4455'))
def test_get_ipv6_addr_by_EUI64_with_IPv4_prefix(self):
ipv4_prefix = '10.0.8'
mac = '00:16:3e:33:44:55'
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
ipv4_prefix, mac)
def test_get_ipv6_addr_by_EUI64_bad_cidr_type(self):
bad_cidr = 123
mac = '00:16:3e:33:44:55'
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
bad_cidr, mac)
def test_get_ipv6_addr_by_EUI64_bad_cidr_value(self):
bad_cidr = 'bb'
mac = '00:16:3e:33:44:55'
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
bad_cidr, mac)
def test_get_ipv6_addr_by_EUI64_bad_mac_value(self):
cidr = '2001:db8::'
bad_mac = '00:16:3e:33:44:5Z'
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
cidr, bad_mac)
def test_get_ipv6_addr_by_EUI64_bad_mac_type(self):
cidr = '2001:db8::'
bad_mac = 99999999999999999999
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
cidr, bad_mac)

View File

@@ -16,5 +16,18 @@
class FakeAuthProvider(object):
def __init__(self, creds_dict={}):
self.credentials = FakeCredentials(creds_dict)
def auth_request(self, method, url, headers=None, body=None, filters=None):
return url, headers, body
def base_url(self, filters, auth_data=None):
return "https://example.com"
class FakeCredentials(object):
def __init__(self, creds_dict):
for key in creds_dict.keys():
setattr(self, key, creds_dict[key])

View File

@@ -0,0 +1,180 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# 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 copy
from tempest_lib import auth
from tempest_lib import exceptions
from tempest_lib.services.identity.v2 import token_client as v2_client
from tempest_lib.services.identity.v3 import token_client as v3_client
from tempest_lib.tests import base
from tempest_lib.tests import fake_identity
class CredentialsTests(base.TestCase):
attributes = {}
credentials_class = auth.Credentials
def _get_credentials(self, attributes=None):
if attributes is None:
attributes = self.attributes
return self.credentials_class(**attributes)
def _check(self, credentials, credentials_class, filled):
# Check the right version of credentials has been returned
self.assertIsInstance(credentials, credentials_class)
# Check the id attributes are filled in
attributes = [x for x in credentials.ATTRIBUTES if (
'_id' in x and x != 'domain_id')]
for attr in attributes:
if filled:
self.assertIsNotNone(getattr(credentials, attr))
else:
self.assertIsNone(getattr(credentials, attr))
def test_create(self):
creds = self._get_credentials()
self.assertEqual(self.attributes, creds._initial)
def test_create_invalid_attr(self):
self.assertRaises(exceptions.InvalidCredentials,
self._get_credentials,
attributes=dict(invalid='fake'))
def test_is_valid(self):
creds = self._get_credentials()
self.assertRaises(NotImplementedError, creds.is_valid)
class KeystoneV2CredentialsTests(CredentialsTests):
attributes = {
'username': 'fake_username',
'password': 'fake_password',
'tenant_name': 'fake_tenant_name'
}
identity_response = fake_identity._fake_v2_response
credentials_class = auth.KeystoneV2Credentials
tokenclient_class = v2_client.TokenClientJSON
identity_version = 'v2'
def setUp(self):
super(KeystoneV2CredentialsTests, self).setUp()
self.stubs.Set(self.tokenclient_class, 'raw_request',
self.identity_response)
def _verify_credentials(self, credentials_class, creds_dict, filled=True):
creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL,
fill_in=filled,
identity_version=self.identity_version,
**creds_dict)
self._check(creds, credentials_class, filled)
def test_get_credentials(self):
self._verify_credentials(credentials_class=self.credentials_class,
creds_dict=self.attributes)
def test_get_credentials_not_filled(self):
self._verify_credentials(credentials_class=self.credentials_class,
creds_dict=self.attributes,
filled=False)
def test_is_valid(self):
creds = self._get_credentials()
self.assertTrue(creds.is_valid())
def _test_is_not_valid(self, ignore_key):
creds = self._get_credentials()
for attr in self.attributes.keys():
if attr == ignore_key:
continue
temp_attr = getattr(creds, attr)
delattr(creds, attr)
self.assertFalse(creds.is_valid(),
"Credentials should be invalid without %s" % attr)
setattr(creds, attr, temp_attr)
def test_is_not_valid(self):
# NOTE(mtreinish): A KeystoneV2 credential object is valid without
# a tenant_name. So skip that check. See tempest.auth for the valid
# credential requirements
self._test_is_not_valid('tenant_name')
def test_reset_all_attributes(self):
creds = self._get_credentials()
initial_creds = copy.deepcopy(creds)
set_attr = creds.__dict__.keys()
missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
# Set all unset attributes, then reset
for attr in missing_attr:
setattr(creds, attr, 'fake' + attr)
creds.reset()
# Check reset credentials are same as initial ones
self.assertEqual(creds, initial_creds)
def test_reset_single_attribute(self):
creds = self._get_credentials()
initial_creds = copy.deepcopy(creds)
set_attr = creds.__dict__.keys()
missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
# Set one unset attributes, then reset
for attr in missing_attr:
setattr(creds, attr, 'fake' + attr)
creds.reset()
# Check reset credentials are same as initial ones
self.assertEqual(creds, initial_creds)
class KeystoneV3CredentialsTests(KeystoneV2CredentialsTests):
attributes = {
'username': 'fake_username',
'password': 'fake_password',
'project_name': 'fake_project_name',
'user_domain_name': 'fake_domain_name'
}
credentials_class = auth.KeystoneV3Credentials
identity_response = fake_identity._fake_v3_response
tokenclient_class = v3_client.V3TokenClientJSON
identity_version = 'v3'
def test_is_not_valid(self):
# NOTE(mtreinish) For a Keystone V3 credential object a project name
# is not required to be valid, so we skip that check. See tempest.auth
# for the valid credential requirements
self._test_is_not_valid('project_name')
def test_synced_attributes(self):
attributes = self.attributes
# Create V3 credentials with tenant instead of project, and user_domain
for attr in ['project_id', 'user_domain_id']:
attributes[attr] = 'fake_' + attr
creds = self._get_credentials(attributes)
self.assertEqual(creds.project_name, creds.tenant_name)
self.assertEqual(creds.project_id, creds.tenant_id)
self.assertEqual(creds.user_domain_name, creds.project_domain_name)
self.assertEqual(creds.user_domain_id, creds.project_domain_id)
# Replace user_domain with project_domain
del attributes['user_domain_name']
del attributes['user_domain_id']
del attributes['project_name']
del attributes['project_id']
for attr in ['project_domain_name', 'project_domain_id',
'tenant_name', 'tenant_id']:
attributes[attr] = 'fake_' + attr
self.assertEqual(creds.tenant_name, creds.project_name)
self.assertEqual(creds.tenant_id, creds.project_id)
self.assertEqual(creds.project_domain_name, creds.user_domain_name)
self.assertEqual(creds.project_domain_id, creds.user_domain_id)

View File

@@ -31,8 +31,9 @@ class BaseRestClientTestClass(base.TestCase):
def setUp(self):
super(BaseRestClientTestClass, self).setUp()
self.fake_auth_provider = fake_auth_provider.FakeAuthProvider()
self.rest_client = rest_client.RestClient(
fake_auth_provider.FakeAuthProvider(), None, None)
self.fake_auth_provider, None, None)
self.stubs.Set(httplib2.Http, 'request', self.fake_http.request)
self.useFixture(mockpatch.PatchObject(self.rest_client,
'_log_request'))
@@ -437,6 +438,59 @@ class TestRestClientUtils(BaseRestClientTestClass):
self.rest_client.wait_for_resource_deletion,
'1234')
def test_get_versions(self):
self.rest_client._parse_resp = lambda x: [{'id': 'v1'}, {'id': 'v2'}]
actual_resp, actual_versions = self.rest_client.get_versions()
self.assertEqual(['v1', 'v2'], list(actual_versions))
def test__str__(self):
def get_token():
return "deadbeef"
self.fake_auth_provider.get_token = get_token
self.assertIsNotNone(str(self.rest_client))
class TestProperties(BaseRestClientTestClass):
def setUp(self):
self.fake_http = fake_http.fake_httplib2()
super(TestProperties, self).setUp()
creds_dict = {
'username': 'test-user',
'user_id': 'test-user_id',
'tenant_name': 'test-tenant_name',
'tenant_id': 'test-tenant_id',
'password': 'test-password'
}
self.rest_client = rest_client.RestClient(
fake_auth_provider.FakeAuthProvider(creds_dict=creds_dict),
None, None)
def test_properties(self):
self.assertEqual('test-user', self.rest_client.user)
self.assertEqual('test-user_id', self.rest_client.user_id)
self.assertEqual('test-tenant_name', self.rest_client.tenant_name)
self.assertEqual('test-tenant_id', self.rest_client.tenant_id)
self.assertEqual('test-password', self.rest_client.password)
self.rest_client.api_version = 'v1'
expected = {'api_version': 'v1',
'endpoint_type': 'publicURL',
'region': None,
'service': None,
'skip_path': True}
self.rest_client.skip_path()
self.assertEqual(expected, self.rest_client.filters)
self.rest_client.reset_path()
self.rest_client.api_version = 'v1'
expected = {'api_version': 'v1',
'endpoint_type': 'publicURL',
'region': None,
'service': None}
self.assertEqual(expected, self.rest_client.filters)
class TestExpectedSuccess(BaseRestClientTestClass):
@@ -483,3 +537,33 @@ class TestExpectedSuccess(BaseRestClientTestClass):
read_code = 202
self.assertRaises(AssertionError, self.rest_client.expected_success,
expected_code, read_code)
class TestResponseBody(base.TestCase):
def test_str(self):
response = {'status': 200}
body = {'key1': 'value1'}
actual = rest_client.ResponseBody(response, body)
self.assertEqual("response: %s\nBody: %s" % (response, body),
str(actual))
class TestResponseBodyData(base.TestCase):
def test_str(self):
response = {'status': 200}
data = 'data1'
actual = rest_client.ResponseBodyData(response, data)
self.assertEqual("response: %s\nBody: %s" % (response, data),
str(actual))
class TestResponseBodyList(base.TestCase):
def test_str(self):
response = {'status': 200}
body = ['value1', 'value2', 'value3']
actual = rest_client.ResponseBodyList(response, body)
self.assertEqual("response: %s\nBody: %s" % (response, body),
str(actual))

View File

@@ -0,0 +1,188 @@
# Copyright 2014 OpenStack Foundation
#
# 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 socket
import time
import mock
import testtools
from tempest_lib.common import ssh
from tempest_lib import exceptions
from tempest_lib.tests import base
class TestSshClient(base.TestCase):
@mock.patch('paramiko.RSAKey.from_private_key')
@mock.patch('six.StringIO')
def test_pkey_calls_paramiko_RSAKey(self, cs_mock, rsa_mock):
cs_mock.return_value = mock.sentinel.csio
pkey = 'mykey'
ssh.Client('localhost', 'root', pkey=pkey)
rsa_mock.assert_called_once_with(mock.sentinel.csio)
cs_mock.assert_called_once_with('mykey')
rsa_mock.reset_mock()
cs_mock.reset_mock()
pkey = mock.sentinel.pkey
# Shouldn't call out to load a file from RSAKey, since
# a sentinel isn't a basestring...
ssh.Client('localhost', 'root', pkey=pkey)
self.assertEqual(0, rsa_mock.call_count)
self.assertEqual(0, cs_mock.call_count)
def _set_ssh_connection_mocks(self):
client_mock = mock.MagicMock()
client_mock.connect.return_value = True
return (self.patch('paramiko.SSHClient'),
self.patch('paramiko.AutoAddPolicy'),
client_mock)
def test_get_ssh_connection(self):
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
s_mock = self.patch('time.sleep')
c_mock.return_value = client_mock
aa_mock.return_value = mock.sentinel.aa
# Test normal case for successful connection on first try
client = ssh.Client('localhost', 'root', timeout=2)
client._get_ssh_connection(sleep=1)
aa_mock.assert_called_once_with()
client_mock.set_missing_host_key_policy.assert_called_once_with(
mock.sentinel.aa)
expected_connect = [mock.call(
'localhost',
username='root',
pkey=None,
key_filename=None,
look_for_keys=False,
timeout=10.0,
password=None
)]
self.assertEqual(expected_connect, client_mock.connect.mock_calls)
self.assertEqual(0, s_mock.call_count)
def test_get_ssh_connection_two_attemps(self):
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
c_mock.return_value = client_mock
client_mock.connect.side_effect = [
socket.error,
mock.MagicMock()
]
client = ssh.Client('localhost', 'root', timeout=1)
start_time = int(time.time())
client._get_ssh_connection(sleep=1)
end_time = int(time.time())
self.assertLess((end_time - start_time), 4)
self.assertGreater((end_time - start_time), 1)
def test_get_ssh_connection_timeout(self):
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
c_mock.return_value = client_mock
client_mock.connect.side_effect = [
socket.error,
socket.error,
socket.error,
]
client = ssh.Client('localhost', 'root', timeout=2)
start_time = int(time.time())
with testtools.ExpectedException(exceptions.SSHTimeout):
client._get_ssh_connection()
end_time = int(time.time())
self.assertLess((end_time - start_time), 5)
self.assertGreaterEqual((end_time - start_time), 2)
def test_exec_command(self):
gsc_mock = self.patch('tempest_lib.common.ssh.Client.'
'_get_ssh_connection')
ito_mock = self.patch('tempest_lib.common.ssh.Client._is_timed_out')
select_mock = self.patch('select.poll')
client_mock = mock.MagicMock()
tran_mock = mock.MagicMock()
chan_mock = mock.MagicMock()
poll_mock = mock.MagicMock()
def reset_mocks():
gsc_mock.reset_mock()
ito_mock.reset_mock()
select_mock.reset_mock()
poll_mock.reset_mock()
client_mock.reset_mock()
tran_mock.reset_mock()
chan_mock.reset_mock()
select_mock.return_value = poll_mock
gsc_mock.return_value = client_mock
ito_mock.return_value = True
client_mock.get_transport.return_value = tran_mock
tran_mock.open_session.return_value = chan_mock
poll_mock.poll.side_effect = [
[0, 0, 0]
]
# Test for a timeout condition immediately raised
client = ssh.Client('localhost', 'root', timeout=2)
with testtools.ExpectedException(exceptions.TimeoutException):
client.exec_command("test")
chan_mock.fileno.assert_called_once_with()
chan_mock.exec_command.assert_called_once_with("test")
chan_mock.shutdown_write.assert_called_once_with()
SELECT_POLLIN = 1
poll_mock.register.assert_called_once_with(chan_mock, SELECT_POLLIN)
poll_mock.poll.assert_called_once_with(10)
# Test for proper reading of STDOUT and STDERROR and closing
# of all file descriptors.
reset_mocks()
select_mock.return_value = poll_mock
gsc_mock.return_value = client_mock
ito_mock.return_value = False
client_mock.get_transport.return_value = tran_mock
tran_mock.open_session.return_value = chan_mock
poll_mock.poll.side_effect = [
[1, 0, 0]
]
closed_prop = mock.PropertyMock(return_value=True)
type(chan_mock).closed = closed_prop
chan_mock.recv_exit_status.return_value = 0
chan_mock.recv.return_value = ''
chan_mock.recv_stderr.return_value = ''
client = ssh.Client('localhost', 'root', timeout=2)
client.exec_command("test")
chan_mock.fileno.assert_called_once_with()
chan_mock.exec_command.assert_called_once_with("test")
chan_mock.shutdown_write.assert_called_once_with()
SELECT_POLLIN = 1
poll_mock.register.assert_called_once_with(chan_mock, SELECT_POLLIN)
poll_mock.poll.assert_called_once_with(10)
chan_mock.recv_ready.assert_called_once_with()
chan_mock.recv.assert_called_once_with(1024)
chan_mock.recv_stderr_ready.assert_called_once_with()
chan_mock.recv_stderr.assert_called_once_with(1024)
chan_mock.recv_exit_status.assert_called_once_with()
closed_prop.assert_called_once_with()

View File

@@ -25,4 +25,4 @@ from tempest_lib.tests import base
class TestTempest_lib(base.TestCase):
def test_something(self):
pass
pass

View File

@@ -1,15 +1,17 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=0.9.2,<0.10
hacking<0.11,>=0.10.0
coverage>=3.6
discover
python-subunit>=0.0.18
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.2.0 # Apache-2.0
oslotest>=1.2.0 # Apache-2.0
oslosphinx>=2.5.0 # Apache-2.0
oslotest>=1.5.1 # Apache-2.0
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.36,!=1.2.0
mock>=1.0
ddt>=0.4.0

View File

@@ -20,19 +20,16 @@ commands = flake8
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --coverage-package-name='tempest_lib' --testr-args='{posargs}'
commands = python setup.py test --coverage --coverage-package-name='tempest_lib' --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[flake8]
# H803 skipped on purpose per list discussion.
# E125 skipped as it is invalid PEP-8.
# H402 skipped because some docstrings aren't sentences
# E123 skipped because it is ignored by default in the default pep8
# E129 skipped because it is too limiting when combined with other rules
# H305 skipped because it is inconsistent between python versions
show-source = True
ignore = E125,H803,H402,E123,E129,H305
ignore = E125,E123,E129
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build