Merge "Add admin role on domain for v3"
diff --git a/HACKING.rst b/HACKING.rst
index d3ac5c6..ec7ff6a 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -132,16 +132,18 @@
Set-up is split in a series of steps (setup stages), which can be overwritten
by test classes. Set-up stages are:
- - `skip_checks`
- - `setup_credentials`
- - `setup_clients`
- - `resource_setup`
+
+- `skip_checks`
+- `setup_credentials`
+- `setup_clients`
+- `resource_setup`
Tear-down is also split in a series of steps (teardown stages), which are
stacked for execution only if the corresponding setup stage had been
reached during the setup phase. Tear-down stages are:
- - `clear_credentials` (defined in the base test class)
- - `resource_cleanup`
+
+- `clear_credentials` (defined in the base test class)
+- `resource_cleanup`
Skipping Tests
--------------
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index d610dc5..9a7ce15 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -348,11 +348,14 @@
service catalog should be in a standard format (which is going to be
standardized at the keystone level).
Tempest expects URLs in the Service catalog in the following format:
- * ``http://example.com:1234/<version-info>``
+
+ * ``http://example.com:1234/<version-info>``
+
Examples:
- * Good - ``http://example.com:1234/v2.0``
- * Wouldn’t work - ``http://example.com:1234/xyz/v2.0/``
- (adding prefix/suffix around version etc)
+
+ * Good - ``http://example.com:1234/v2.0``
+ * Wouldn’t work - ``http://example.com:1234/xyz/v2.0/``
+ (adding prefix/suffix around version etc)
Service Feature Configuration
-----------------------------
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index fc05b12..3568470 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -24,6 +24,7 @@
Those should be defined under respective section of each service.
For example::
+
[compute]
min_microversion = None
max_microversion = latest
@@ -159,7 +160,8 @@
Notes about Compute Microversion Tests
-"""""""""""""""""""""""""""""""""""
+""""""""""""""""""""""""""""""""""""""
+
Some of the compute Microversion tests have been already implemented
with the Microversion testing framework. So for further tests only
step 4 is needed.
diff --git a/releasenotes/notes/support-chunked-encoding-d71f53225f68edf3.yaml b/releasenotes/notes/support-chunked-encoding-d71f53225f68edf3.yaml
new file mode 100644
index 0000000..eb45523
--- /dev/null
+++ b/releasenotes/notes/support-chunked-encoding-d71f53225f68edf3.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - The RestClient (in tempest.lib.common.rest_client) now supports POSTing
+ and PUTing data with chunked transfer encoding. Just pass an `iterable`
+ object as the `body` argument and set the `chunked` argument to `True`.
+ - A new generator called `chunkify` is added in
+ tempest.lib.common.utils.data_utils that yields fixed-size chunks (slices)
+ from a Python sequence.
+
diff --git a/requirements.txt b/requirements.txt
index d567082..f41d2a7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,17 +9,17 @@
netaddr!=0.7.16,>=0.7.12 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD
pyOpenSSL>=0.14 # Apache-2.0
-oslo.concurrency>=3.5.0 # Apache-2.0
+oslo.concurrency>=3.8.0 # Apache-2.0
oslo.config>=3.9.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0
six>=1.9.0 # MIT
-fixtures<2.0,>=1.3.1 # Apache-2.0/BSD
+fixtures>=3.0.0 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
PyYAML>=3.1.0 # MIT
stevedore>=1.10.0 # Apache-2.0
PrettyTable<0.8,>=0.7 # BSD
-os-testr>=0.4.1 # Apache-2.0
+os-testr>=0.7.0 # Apache-2.0
urllib3>=1.15.1 # MIT
diff --git a/setup.cfg b/setup.cfg
index 0ddb898..24e0214 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,7 +5,7 @@
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/
+home-page = http://docs.openstack.org/developer/tempest/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
diff --git a/tempest/api/compute/admin/test_simple_tenant_usage.py b/tempest/api/compute/admin/test_simple_tenant_usage.py
index 8986db8..a4ed8dc 100644
--- a/tempest/api/compute/admin/test_simple_tenant_usage.py
+++ b/tempest/api/compute/admin/test_simple_tenant_usage.py
@@ -59,7 +59,9 @@
return True
except e.InvalidHTTPResponseBody:
return False
- test.call_until_true(is_valid, duration, 1)
+ self.assertEqual(test.call_until_true(is_valid, duration, 1), True,
+ "%s not return valid response in %s secs" % (
+ func.__name__, duration))
return self.resp
@test.idempotent_id('062c8ae9-9912-4249-8b51-e38d664e926e')
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index c05045e..07423ff 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -204,10 +204,6 @@
@test.idempotent_id('1678d144-ed74-43f8-8e57-ab10dbf9b3c2')
@testtools.skipUnless(CONF.service_available.neutron,
'Neutron service must be available.')
- # The below skipUnless should be removed once Kilo-eol happens.
- @testtools.skipUnless(CONF.compute_feature_enabled.
- allow_duplicate_networks,
- 'Duplicate networks must be allowed')
def test_verify_duplicate_network_nics(self):
# Verify that server creation does not fail when more than one nic
# is created on the same network.
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index f01657b..66aec84 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -226,6 +226,35 @@
self.client.start_server(self.server_id)
+ @test.idempotent_id('b68bd8d6-855d-4212-b59b-2e704044dace')
+ @test.services('volume')
+ def test_rebuild_server_with_volume_attached(self):
+ # create a new volume and attach it to the server
+ volume = self.volumes_client.create_volume(
+ size=CONF.volume.volume_size)
+ volume = volume['volume']
+ self.addCleanup(self.volumes_client.delete_volume, volume['id'])
+ waiters.wait_for_volume_status(self.volumes_client, volume['id'],
+ 'available')
+
+ self.client.attach_volume(self.server_id, volumeId=volume['id'])
+ self.addCleanup(waiters.wait_for_volume_status, self.volumes_client,
+ volume['id'], 'available')
+ self.addCleanup(self.client.detach_volume,
+ self.server_id, volume['id'])
+ waiters.wait_for_volume_status(self.volumes_client, volume['id'],
+ 'in-use')
+
+ # run general rebuild test
+ self.test_rebuild_server()
+
+ # make sure the volume is attached to the instance after rebuild
+ vol_after_rebuild = self.volumes_client.show_volume(volume['id'])
+ vol_after_rebuild = vol_after_rebuild['volume']
+ self.assertEqual('in-use', vol_after_rebuild['status'])
+ self.assertEqual(self.server_id,
+ vol_after_rebuild['attachments'][0]['server_id'])
+
def _test_resize_server_confirm(self, stop=False):
# The server's RAM and disk space should be modified to that of
# the provided flavor
@@ -312,7 +341,8 @@
image1_id = data_utils.parse_image_id(resp['location'])
self.addCleanup(_clean_oldest_backup, image1_id)
- self.os.image_client.wait_for_image_status(image1_id, 'active')
+ waiters.wait_for_image_status(self.os.image_client,
+ image1_id, 'active')
backup2 = data_utils.rand_name('backup-2')
waiters.wait_for_server_status(self.client, self.server_id, 'ACTIVE')
@@ -322,7 +352,8 @@
name=backup2).response
image2_id = data_utils.parse_image_id(resp['location'])
self.addCleanup(self.os.image_client.delete_image, image2_id)
- self.os.image_client.wait_for_image_status(image2_id, 'active')
+ waiters.wait_for_image_status(self.os.image_client,
+ image2_id, 'active')
# verify they have been created
properties = {
diff --git a/tempest/api/identity/admin/v2/test_users_negative.py b/tempest/api/identity/admin/v2/test_users_negative.py
index 46ecba1..5fda4c14 100644
--- a/tempest/api/identity/admin/v2/test_users_negative.py
+++ b/tempest/api/identity/admin/v2/test_users_negative.py
@@ -83,13 +83,14 @@
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
+
+ # Unset the token to allow further tests to generate a new token
+ self.addCleanup(self.client.auth_provider.clear_auth)
+
self.assertRaises(lib_exc.Unauthorized, self.users_client.create_user,
self.alt_user, self.alt_password,
self.data.tenant['id'], self.alt_email)
- # Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
-
@test.attr(type=['negative'])
@test.idempotent_id('23a2f3da-4a1a-41da-abdd-632328a861ad')
def test_create_user_with_enabled_non_bool(self):
@@ -119,11 +120,12 @@
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.users_client.update_user,
- self.alt_user)
# Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
+ self.addCleanup(self.client.auth_provider.clear_auth)
+
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.update_user,
+ self.alt_user)
@test.attr(type=['negative'])
@test.idempotent_id('424868d5-18a7-43e1-8903-a64f95ee3aac')
@@ -159,11 +161,12 @@
token = self.client.auth_provider.get_token()
# Delete the token from database
self.client.delete_token(token)
- self.assertRaises(lib_exc.Unauthorized, self.users_client.delete_user,
- self.alt_user)
# Unset the token to allow further tests to generate a new token
- self.client.auth_provider.clear_auth()
+ self.addCleanup(self.client.auth_provider.clear_auth)
+
+ self.assertRaises(lib_exc.Unauthorized, self.users_client.delete_user,
+ self.alt_user)
@test.attr(type=['negative'])
@test.idempotent_id('593a4981-f6d4-460a-99a1-57a78bf20829')
@@ -229,8 +232,11 @@
# Request to get list of users without a valid token should fail
token = self.client.auth_provider.get_token()
self.client.delete_token(token)
+
+ # Unset the token to allow further tests to generate a new token
+ self.addCleanup(self.client.auth_provider.clear_auth)
+
self.assertRaises(lib_exc.Unauthorized, self.users_client.list_users)
- self.client.auth_provider.clear_auth()
@test.attr(type=['negative'])
@test.idempotent_id('f5d39046-fc5f-425c-b29e-bac2632da28e')
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index cad22f3..6d5559d 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -17,6 +17,7 @@
from tempest.api.image import base
from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
from tempest import exceptions
from tempest import test
@@ -95,7 +96,7 @@
image_id = body.get('id')
self.assertEqual('New Http Image', body.get('name'))
self.assertFalse(body.get('is_public'))
- self.client.wait_for_image_status(image_id, 'active')
+ waiters.wait_for_image_status(self.client, image_id, 'active')
self.client.show_image(image_id)
@test.idempotent_id('05b19d55-140c-40d0-b36b-fafd774d421b')
@@ -305,7 +306,7 @@
@test.idempotent_id('01752c1c-0275-4de3-9e5b-876e44541928')
def test_list_image_metadata(self):
# All metadata key/value pairs for an image should be returned
- resp_metadata = self.client.get_image_meta(self.image_id)
+ resp_metadata = self.client.check_image(self.image_id)
expected = {'key1': 'value1'}
self.assertEqual(expected, resp_metadata['properties'])
@@ -313,12 +314,12 @@
def test_update_image_metadata(self):
# The metadata for the image should match the updated values
req_metadata = {'key1': 'alt1', 'key2': 'value2'}
- metadata = self.client.get_image_meta(self.image_id)
+ metadata = self.client.check_image(self.image_id)
self.assertEqual(metadata['properties'], {'key1': 'value1'})
metadata['properties'].update(req_metadata)
metadata = self.client.update_image(
self.image_id, properties=metadata['properties'])['image']
- resp_metadata = self.client.get_image_meta(self.image_id)
+ resp_metadata = self.client.check_image(self.image_id)
expected = {'key1': 'alt1', 'key2': 'value2'}
self.assertEqual(expected, resp_metadata['properties'])
diff --git a/tempest/api/network/test_security_groups_negative.py b/tempest/api/network/test_security_groups_negative.py
index b9765c8..a3b0a82 100644
--- a/tempest/api/network/test_security_groups_negative.py
+++ b/tempest/api/network/test_security_groups_negative.py
@@ -153,6 +153,7 @@
# Create rule for icmp protocol with invalid ports
states = [(1, 256, 'Invalid value for ICMP code'),
+ (-1, 25, 'Invalid value'),
(None, 6, 'ICMP type (port-range-min) is missing'),
(300, 1, 'Invalid value for ICMP type')]
for pmin, pmax, msg in states:
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index dc926e0..a88e4f4 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -20,7 +20,6 @@
import zlib
import six
-from six import moves
from tempest.api.object_storage import base
from tempest.common import custom_matchers
@@ -201,8 +200,8 @@
status, _, resp_headers = self.object_client.put_object_with_chunk(
container=self.container_name,
name=object_name,
- contents=moves.cStringIO(data),
- chunk_size=512)
+ contents=data_utils.chunkify(data, 512)
+ )
self.assertHeaders(resp_headers, 'Object', 'PUT')
# check uploaded content
diff --git a/tempest/api/volume/test_qos.py b/tempest/api/volume/admin/test_qos.py
similarity index 100%
rename from tempest/api/volume/test_qos.py
rename to tempest/api/volume/admin/test_qos.py
diff --git a/tempest/api/volume/admin/test_volume_quotas.py b/tempest/api/volume/admin/test_volume_quotas.py
index b2e52bb..cf05f5f 100644
--- a/tempest/api/volume/admin/test_volume_quotas.py
+++ b/tempest/api/volume/admin/test_volume_quotas.py
@@ -30,6 +30,11 @@
super(BaseVolumeQuotasAdminV2TestJSON, cls).setup_credentials()
cls.demo_tenant_id = cls.os.credentials.tenant_id
+ def _delete_volume(self, volume_id):
+ # Delete the specified volume using admin credentials
+ self.admin_volume_client.delete_volume(volume_id)
+ self.admin_volume_client.wait_for_resource_deletion(volume_id)
+
@test.idempotent_id('59eada70-403c-4cef-a2a3-a8ce2f1b07a0')
def test_list_quotas(self):
quotas = (self.quotas_client.show_quota_set(self.demo_tenant_id)
@@ -83,8 +88,7 @@
self.demo_tenant_id)['quota_set']
volume = self.create_volume()
- self.addCleanup(self.admin_volume_client.delete_volume,
- volume['id'])
+ self.addCleanup(self._delete_volume, volume['id'])
new_quota_usage = self.quotas_client.show_quota_usage(
self.demo_tenant_id)['quota_set']
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index e52216f..5975231 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -125,7 +125,7 @@
disk_format=CONF.volume.disk_format)['os-volume_upload_image']
image_id = body["image_id"]
self.addCleanup(self._cleanup_image, image_id)
- self.image_client.wait_for_image_status(image_id, 'active')
+ waiters.wait_for_image_status(self.image_client, image_id, 'active')
waiters.wait_for_volume_status(self.client,
self.volume['id'], 'available')
diff --git a/tempest/clients.py b/tempest/clients.py
index b0f779f..19f1a2a 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -131,7 +131,8 @@
from tempest.services.identity.v3.json.users_clients import \
UsersClient as UsersV3Client
from tempest.services.image.v1.json.images_client import ImagesClient
-from tempest.services.image.v2.json.images_client import ImagesClientV2
+from tempest.services.image.v2.json.images_client import \
+ ImagesClient as ImagesV2Client
from tempest.services.network.json.routers_client import RoutersClient
from tempest.services.object_storage.account_client import AccountClient
from tempest.services.object_storage.container_client import ContainerClient
@@ -331,7 +332,7 @@
build_interval=CONF.image.build_interval,
build_timeout=CONF.image.build_timeout,
**self.default_params)
- self.image_client_v2 = ImagesClientV2(
+ self.image_client_v2 = ImagesV2Client(
self.auth_provider,
CONF.image.catalog_type,
CONF.image.region or CONF.identity.region,
diff --git a/tempest/cmd/javelin.py b/tempest/cmd/javelin.py
index 9b3cac7..17ee618 100755
--- a/tempest/cmd/javelin.py
+++ b/tempest/cmd/javelin.py
@@ -233,7 +233,7 @@
**object_storage_params)
self.containers = container_client.ContainerClient(
_auth, **object_storage_params)
- self.images = images_client.ImagesClientV2(
+ self.images = images_client.ImagesClient(
_auth,
CONF.image.catalog_type,
CONF.image.region or CONF.identity.region,
diff --git a/tempest/common/credentials_factory.py b/tempest/common/credentials_factory.py
index 7c73ada..286e01f 100644
--- a/tempest/common/credentials_factory.py
+++ b/tempest/common/credentials_factory.py
@@ -73,8 +73,8 @@
# the test should be skipped else it would fail.
identity_version = identity_version or CONF.identity.auth_version
if CONF.auth.use_dynamic_credentials or force_tenant_isolation:
- admin_creds = get_configured_credentials(
- 'identity_admin', fill_in=True, identity_version=identity_version)
+ admin_creds = get_configured_admin_credentials(
+ fill_in=True, identity_version=identity_version)
return dynamic_creds.DynamicCredentialProvider(
name=name,
network_resources=network_resources,
@@ -111,8 +111,8 @@
is_admin = False
else:
try:
- get_configured_credentials('identity_admin', fill_in=False,
- identity_version=identity_version)
+ get_configured_admin_credentials(fill_in=False,
+ identity_version=identity_version)
except exceptions.InvalidConfiguration:
is_admin = False
return is_admin
@@ -163,44 +163,32 @@
# Read credentials from configuration, builds a Credentials object
# based on the specified or configured version
-def get_configured_credentials(credential_type, fill_in=True,
- identity_version=None):
+def get_configured_admin_credentials(fill_in=True, identity_version=None):
identity_version = identity_version or CONF.identity.auth_version
if identity_version not in ('v2', 'v3'):
raise exceptions.InvalidConfiguration(
'Unsupported auth version: %s' % identity_version)
- if credential_type not in CREDENTIAL_TYPES:
- raise exceptions.InvalidCredentials()
- conf_attributes = ['username', 'password', 'project_name']
+ conf_attributes = ['username', 'password',
+ 'project_name']
if identity_version == 'v3':
conf_attributes.append('domain_name')
# Read the parts of credentials from config
params = DEFAULT_PARAMS.copy()
- section, prefix = CREDENTIAL_TYPES[credential_type]
for attr in conf_attributes:
- _section = getattr(CONF, section)
- if prefix is None:
- params[attr] = getattr(_section, attr)
- else:
- params[attr] = getattr(_section, prefix + "_" + attr)
- # NOTE(andreaf) v2 API still uses tenants, so we must translate project
- # to tenant before building the Credentials object
- if identity_version == 'v2':
- params['tenant_name'] = params.get('project_name')
- params.pop('project_name', None)
+ params[attr] = getattr(CONF.auth, 'admin_' + attr)
# Build and validate credentials. We are reading configured credentials,
# so validate them even if fill_in is False
credentials = get_credentials(fill_in=fill_in,
identity_version=identity_version, **params)
if not fill_in:
if not credentials.is_valid():
- msg = ("The %s credentials are incorrectly set in the config file."
- " Double check that all required values are assigned" %
- credential_type)
- raise exceptions.InvalidConfiguration(msg)
+ msg = ("The admin credentials are incorrectly set in the config "
+ "file for identity version %s. Double check that all "
+ "required values are assigned.")
+ raise exceptions.InvalidConfiguration(msg % identity_version)
return credentials
@@ -228,19 +216,10 @@
# === Credential / client managers
-class ConfiguredUserManager(clients.Manager):
- """Manager that uses user credentials for its managed client objects"""
-
- def __init__(self, service=None):
- super(ConfiguredUserManager, self).__init__(
- credentials=get_configured_credentials('user'),
- service=service)
-
-
class AdminManager(clients.Manager):
"""Manager that uses admin credentials for its managed client objects"""
def __init__(self, service=None):
super(AdminManager, self).__init__(
- credentials=get_configured_credentials('identity_admin'),
+ credentials=get_configured_admin_credentials(),
service=service)
diff --git a/tempest/common/preprov_creds.py b/tempest/common/preprov_creds.py
index 51f723b..7350222 100644
--- a/tempest/common/preprov_creds.py
+++ b/tempest/common/preprov_creds.py
@@ -43,6 +43,11 @@
class PreProvisionedCredentialProvider(cred_provider.CredentialProvider):
+ # Exclude from the hash fields specific to v2 or v3 identity API
+ # i.e. only include user*, project*, tenant* and password
+ HASH_CRED_FIELDS = (set(auth.KeystoneV2Credentials.ATTRIBUTES) &
+ set(auth.KeystoneV3Credentials.ATTRIBUTES))
+
def __init__(self, identity_version, test_accounts_file,
accounts_lock_dir, name=None, credentials_domain=None,
admin_role=None, object_storage_operator_role=None,
@@ -104,6 +109,7 @@
object_storage_operator_role=None,
object_storage_reseller_admin_role=None):
hash_dict = {'roles': {}, 'creds': {}, 'networks': {}}
+
# Loop over the accounts read from the yaml file
for account in accounts:
roles = []
@@ -116,7 +122,9 @@
if 'resources' in account:
resources = account.pop('resources')
temp_hash = hashlib.md5()
- temp_hash.update(six.text_type(account).encode('utf-8'))
+ account_for_hash = dict((k, v) for (k, v) in six.iteritems(account)
+ if k in cls.HASH_CRED_FIELDS)
+ temp_hash.update(six.text_type(account_for_hash).encode('utf-8'))
temp_hash_key = temp_hash.hexdigest()
hash_dict['creds'][temp_hash_key] = account
for role in roles:
@@ -262,13 +270,13 @@
for _hash in self.hash_dict['creds']:
# Comparing on the attributes that are expected in the YAML
init_attributes = creds.get_init_attributes()
+ # Only use the attributes initially used to calculate the hash
+ init_attributes = [x for x in init_attributes if
+ x in self.HASH_CRED_FIELDS]
hash_attributes = self.hash_dict['creds'][_hash].copy()
- if ('user_domain_name' in init_attributes and 'user_domain_name'
- not in hash_attributes):
- # Allow for the case of domain_name populated from config
- domain_name = self.credentials_domain
- hash_attributes['user_domain_name'] = domain_name
- if all([getattr(creds, k) == hash_attributes[k] for
+ # NOTE(andreaf) Not all fields may be available on all credentials
+ # so defaulting to None for that case.
+ if all([getattr(creds, k, None) == hash_attributes.get(k, None) for
k in init_attributes]):
return _hash
raise AttributeError('Invalid credentials %s' % creds)
@@ -351,23 +359,20 @@
return net_creds
def _extend_credentials(self, creds_dict):
- # In case of v3, adds a user_domain_name field to the creds
- # dict if not defined
+ # Add or remove credential domain fields to fit the identity version
+ domain_fields = set(x for x in auth.KeystoneV3Credentials.ATTRIBUTES
+ if 'domain' in x)
+ msg = 'Assuming they are valid in the default domain.'
if self.identity_version == 'v3':
- user_domain_fields = set(['user_domain_name', 'user_domain_id'])
- if not user_domain_fields.intersection(set(creds_dict.keys())):
- creds_dict['user_domain_name'] = self.credentials_domain
- # NOTE(andreaf) In case of v2, replace project with tenant if project
- # is provided and tenant is not
+ if not domain_fields.intersection(set(creds_dict.keys())):
+ msg = 'Using credentials %s for v3 API calls. ' + msg
+ LOG.warning(msg, self._sanitize_creds(creds_dict))
+ creds_dict['domain_name'] = self.credentials_domain
if self.identity_version == 'v2':
- if ('project_name' in creds_dict and
- 'tenant_name' in creds_dict and
- creds_dict['project_name'] != creds_dict['tenant_name']):
- clean_creds = self._sanitize_creds(creds_dict)
- msg = 'Cannot specify project and tenant at the same time %s'
- raise exceptions.InvalidCredentials(msg % clean_creds)
- if ('project_name' in creds_dict and
- 'tenant_name' not in creds_dict):
- creds_dict['tenant_name'] = creds_dict['project_name']
- creds_dict.pop('project_name')
+ if domain_fields.intersection(set(creds_dict.keys())):
+ msg = 'Using credentials %s for v2 API calls. ' + msg
+ LOG.warning(msg, self._sanitize_creds(creds_dict))
+ # Remove all valid domain attributes
+ for attr in domain_fields.intersection(set(creds_dict.keys())):
+ creds_dict.pop(attr)
return creds_dict
diff --git a/tempest/common/utils/file_utils.py b/tempest/common/utils/file_utils.py
deleted file mode 100644
index 43083f4..0000000
--- a/tempest/common/utils/file_utils.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-
-
-def have_effective_read_access(path):
- try:
- fh = open(path, "rb")
- except IOError:
- return False
- fh.close()
- return True
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 95305f3..23d7f88 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -19,6 +19,7 @@
from tempest import exceptions
from tempest.lib.common.utils import misc as misc_utils
from tempest.lib import exceptions as lib_exc
+from tempest.services.image.v1.json import images_client as images_v1_client
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -122,42 +123,44 @@
The client should have a show_image(image_id) method to get the image.
The client should also have build_interval and build_timeout attributes.
"""
- image = client.show_image(image_id)
- # Compute image client return response wrapped in 'image' element
- # which is not case with glance image client.
- if 'image' in image:
- image = image['image']
- start = int(time.time())
+ if isinstance(client, images_v1_client.ImagesClient):
+ # The 'check_image' method is used here because the show_image method
+ # returns image details plus the image itself which is very expensive.
+ # The 'check_image' method returns just image details.
+ show_image = client.check_image
+ else:
+ show_image = client.show_image
- while image['status'] != status:
- time.sleep(client.build_interval)
- image = client.show_image(image_id)
- # Compute image client return response wrapped in 'image' element
- # which is not case with glance image client.
+ current_status = 'An unknown status'
+ start = int(time.time())
+ while int(time.time()) - start < client.build_timeout:
+ image = show_image(image_id)
+ # Compute image client returns response wrapped in 'image' element
+ # which is not case with Glance image client.
if 'image' in image:
image = image['image']
- status_curr = image['status']
- if status_curr == 'ERROR':
+
+ current_status = image['status']
+ if current_status == status:
+ return
+ if current_status.lower() == 'killed':
+ raise exceptions.ImageKilledException(image_id=image_id,
+ status=status)
+ if current_status.lower() == 'error':
raise exceptions.AddImageException(image_id=image_id)
- # check the status again to avoid a false negative where we hit
- # the timeout at the same time that the image reached the expected
- # status
- if status_curr == status:
- return
+ time.sleep(client.build_interval)
- if int(time.time()) - start >= client.build_timeout:
- message = ('Image %(image_id)s failed to reach %(status)s state'
- '(current state %(status_curr)s) '
- 'within the required time (%(timeout)s s).' %
- {'image_id': image_id,
- 'status': status,
- 'status_curr': status_curr,
- 'timeout': client.build_timeout})
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
+ message = ('Image %(image_id)s failed to reach %(status)s state '
+ '(current state %(current_status)s) within the required '
+ 'time (%(timeout)s s).' % {'image_id': image_id,
+ 'status': status,
+ 'current_status': current_status,
+ 'timeout': client.build_timeout})
+ caller = misc_utils.find_test_caller()
+ if caller:
+ message = '(%s) %s' % (caller, message)
+ raise exceptions.TimeoutException(message)
def wait_for_volume_status(client, volume_id, status):
diff --git a/tempest/config.py b/tempest/config.py
index f5125b5..71b25d0 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -370,14 +370,6 @@
'encrypted volume to a running server instance? This may '
'depend on the combination of compute_driver in nova and '
'the volume_driver(s) in cinder.'),
- # TODO(mriedem): Remove allow_duplicate_networks once kilo-eol happens
- # since the option was removed from nova in Liberty and is the default
- # behavior starting in Liberty.
- cfg.BoolOpt('allow_duplicate_networks',
- default=False,
- help='Does the test environment support creating instances '
- 'with multiple ports on the same network? This is only '
- 'valid when using Neutron.'),
cfg.BoolOpt('config_drive',
default=True,
help='Enable special configuration drive with metadata.'),
diff --git a/tempest/lib/api_schema/response/compute/v2_1/images.py b/tempest/lib/api_schema/response/compute/v2_1/images.py
index daab898..b0f1934 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/images.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/images.py
@@ -77,7 +77,7 @@
'properties': {
'id': {'type': 'string'},
'links': image_links,
- 'name': {'type': 'string'}
+ 'name': {'type': ['string', 'null']}
},
'additionalProperties': False,
'required': ['id', 'links', 'name']
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index ffcc4fb..974ba82 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -605,6 +605,7 @@
"""
ATTRIBUTES = []
+ COLLISIONS = []
def __init__(self, **kwargs):
"""Enforce the available attributes at init time (only).
@@ -616,6 +617,13 @@
self._apply_credentials(kwargs)
def _apply_credentials(self, attr):
+ for (key1, key2) in self.COLLISIONS:
+ val1 = attr.get(key1)
+ val2 = attr.get(key2)
+ if val1 and val2 and val1 != val2:
+ msg = ('Cannot have conflicting values for %s and %s' %
+ (key1, key2))
+ raise exceptions.InvalidCredentials(msg)
for key in attr.keys():
if key in self.ATTRIBUTES:
setattr(self, key, attr[key])
@@ -673,7 +681,33 @@
class KeystoneV2Credentials(Credentials):
ATTRIBUTES = ['username', 'password', 'tenant_name', 'user_id',
- 'tenant_id']
+ 'tenant_id', 'project_id', 'project_name']
+ COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
+
+ def __str__(self):
+ """Represent only attributes included in self.ATTRIBUTES"""
+ attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+ _repr = dict((k, getattr(self, k)) for k in attrs)
+ return str(_repr)
+
+ def __setattr__(self, key, value):
+ # NOTE(andreaf) In order to ease the migration towards 'project' we
+ # support v2 credentials configured with 'project' and translate it
+ # to tenant on the fly. The original kwargs are stored for clients
+ # that may rely on them. We also set project when tenant is defined
+ # so clients can rely on project being part of credentials.
+ parent = super(KeystoneV2Credentials, self)
+ # for project_* set tenant only
+ if key == 'project_id':
+ parent.__setattr__('tenant_id', value)
+ elif key == 'project_name':
+ parent.__setattr__('tenant_name', value)
+ if key == 'tenant_id':
+ parent.__setattr__('project_id', value)
+ elif key == 'tenant_name':
+ parent.__setattr__('project_name', value)
+ # trigger default behaviour for all attributes
+ parent.__setattr__(key, value)
def is_valid(self):
"""Check of credentials (no API call)
@@ -684,9 +718,6 @@
return None not in (self.username, self.password)
-COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
-
-
class KeystoneV3Credentials(Credentials):
"""Credentials suitable for the Keystone Identity V3 API"""
@@ -694,16 +725,7 @@
'project_domain_id', 'project_domain_name', 'project_id',
'project_name', 'tenant_id', 'tenant_name', 'user_domain_id',
'user_domain_name', 'user_id']
-
- def _apply_credentials(self, attr):
- for (key1, key2) in COLLISIONS:
- val1 = attr.get(key1)
- val2 = attr.get(key2)
- if val1 and val2 and val1 != val2:
- msg = ('Cannot have conflicting values for %s and %s' %
- (key1, key2))
- raise exceptions.InvalidCredentials(msg)
- super(KeystoneV3Credentials, self)._apply_credentials(attr)
+ COLLISIONS = [('project_name', 'tenant_name'), ('project_id', 'tenant_id')]
def __setattr__(self, key, value):
parent = super(KeystoneV3Credentials, self)
diff --git a/tempest/lib/cli/base.py b/tempest/lib/cli/base.py
index 54f35f4..a43d002 100644
--- a/tempest/lib/cli/base.py
+++ b/tempest/lib/cli/base.py
@@ -119,7 +119,7 @@
:param merge_stderr: if True the stderr buffer is merged into stdout
:type merge_stderr: boolean
"""
- flags += ' --endpoint-type %s' % endpoint_type
+ flags += ' --os-endpoint-type %s' % endpoint_type
return self.cmd_with_auth(
'nova', action, flags, params, fail_ok, merge_stderr)
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 30750de..179db17 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -243,7 +243,8 @@
details = pattern.format(read_code, expected_code)
raise exceptions.InvalidHttpSuccessCode(details)
- def post(self, url, body, headers=None, extra_headers=False):
+ def post(self, url, body, headers=None, extra_headers=False,
+ chunked=False):
"""Send a HTTP POST request using keystone auth
:param str url: the relative url to send the post request to
@@ -253,11 +254,12 @@
returned by the get_headers() method are to
be used but additional headers are needed in
the request pass them in as a dict.
+ :param bool chunked: sends the body with chunked encoding
:return: a tuple with the first entry containing the response headers
and the second the response body
:rtype: tuple
"""
- return self.request('POST', url, extra_headers, headers, body)
+ return self.request('POST', url, extra_headers, headers, body, chunked)
def get(self, url, headers=None, extra_headers=False):
"""Send a HTTP GET request using keystone service catalog and auth
@@ -306,7 +308,7 @@
"""
return self.request('PATCH', url, extra_headers, headers, body)
- def put(self, url, body, headers=None, extra_headers=False):
+ def put(self, url, body, headers=None, extra_headers=False, chunked=False):
"""Send a HTTP PUT request using keystone service catalog and auth
:param str url: the relative url to send the post request to
@@ -316,11 +318,12 @@
returned by the get_headers() method are to
be used but additional headers are needed in
the request pass them in as a dict.
+ :param bool chunked: sends the body with chunked encoding
:return: a tuple with the first entry containing the response headers
and the second the response body
:rtype: tuple
"""
- return self.request('PUT', url, extra_headers, headers, body)
+ return self.request('PUT', url, extra_headers, headers, body, chunked)
def head(self, url, headers=None, extra_headers=False):
"""Send a HTTP HEAD request using keystone service catalog and auth
@@ -520,7 +523,7 @@
if method != 'HEAD' and not resp_body and resp.status >= 400:
self.LOG.warning("status >= 400 response with empty body")
- def _request(self, method, url, headers=None, body=None):
+ def _request(self, method, url, headers=None, body=None, chunked=False):
"""A simple HTTP request interface."""
# Authenticate the request with the auth provider
req_url, req_headers, req_body = self.auth_provider.auth_request(
@@ -530,7 +533,9 @@
start = time.time()
self._log_request_start(method, req_url)
resp, resp_body = self.raw_request(
- req_url, method, headers=req_headers, body=req_body)
+ req_url, method, headers=req_headers, body=req_body,
+ chunked=chunked
+ )
end = time.time()
self._log_request(method, req_url, resp, secs=(end - start),
req_headers=req_headers, req_body=req_body,
@@ -541,7 +546,7 @@
return resp, resp_body
- def raw_request(self, url, method, headers=None, body=None):
+ def raw_request(self, url, method, headers=None, body=None, chunked=False):
"""Send a raw HTTP request without the keystone catalog or auth
This method sends a HTTP request in the same manner as the request()
@@ -554,17 +559,18 @@
:param str headers: Headers to use for the request if none are specifed
the headers
:param str body: Body to send with the request
+ :param bool chunked: sends the body with chunked encoding
:rtype: tuple
:return: a tuple with the first entry containing the response headers
and the second the response body
"""
if headers is None:
headers = self.get_headers()
- return self.http_obj.request(url, method,
- headers=headers, body=body)
+ return self.http_obj.request(url, method, headers=headers,
+ body=body, chunked=chunked)
def request(self, method, url, extra_headers=False, headers=None,
- body=None):
+ body=None, chunked=False):
"""Send a HTTP request with keystone auth and using the catalog
This method will send an HTTP request using keystone auth in the
@@ -590,6 +596,7 @@
get_headers() method are used. If the request
explicitly requires no headers use an empty dict.
:param str body: Body to send with the request
+ :param bool chunked: sends the body with chunked encoding
:rtype: tuple
:return: a tuple with the first entry containing the response headers
and the second the response body
@@ -629,8 +636,8 @@
except (ValueError, TypeError):
headers = self.get_headers()
- resp, resp_body = self._request(method, url,
- headers=headers, body=body)
+ resp, resp_body = self._request(method, url, headers=headers,
+ body=body, chunked=chunked)
while (resp.status == 413 and
'retry-after' in resp and
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index 9605479..45e5067 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -19,6 +19,8 @@
import string
import uuid
+import six.moves
+
def rand_uuid():
"""Generate a random UUID string
@@ -196,3 +198,10 @@
except TypeError:
raise TypeError('Bad prefix type for generate IPv6 address by '
'EUI-64: %s' % cidr)
+
+
+# Courtesy of http://stackoverflow.com/a/312464
+def chunkify(sequence, chunksize):
+ """Yield successive chunks from `sequence`."""
+ for i in six.moves.xrange(0, len(sequence), chunksize):
+ yield sequence[i:i + chunksize]
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index e78e624..6ed99b4 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -69,12 +69,11 @@
"or False") % attr
def __call__(self, func):
+ @functools.wraps(func)
def _skipper(*args, **kw):
"""Wrapped skipper function."""
testobj = args[0]
if not getattr(testobj, self.attr, False):
raise testtools.TestCase.skipException(self.message)
func(*args, **kw)
- _skipper.__name__ = func.__name__
- _skipper.__doc__ = func.__doc__
return _skipper
diff --git a/tempest/lib/services/compute/base_compute_client.py b/tempest/lib/services/compute/base_compute_client.py
index 9161abb..433c94c 100644
--- a/tempest/lib/services/compute/base_compute_client.py
+++ b/tempest/lib/services/compute/base_compute_client.py
@@ -48,9 +48,9 @@
return headers
def request(self, method, url, extra_headers=False, headers=None,
- body=None):
+ body=None, chunked=False):
resp, resp_body = super(BaseComputeClient, self).request(
- method, url, extra_headers, headers, body)
+ method, url, extra_headers, headers, body, chunked)
if (COMPUTE_MICROVERSION and
COMPUTE_MICROVERSION != api_version_utils.LATEST_MICROVERSION):
api_version_utils.assert_version_header_matches_request(
@@ -67,11 +67,13 @@
:param schema_versions_info: List of dict which provides schema
information with range of valid versions.
- Example -
- schema_versions_info = [
- {'min': None, 'max': '2.1', 'schema': schemav21},
- {'min': '2.2', 'max': '2.9', 'schema': schemav22},
- {'min': '2.10', 'max': None, 'schema': schemav210}]
+
+ Example::
+
+ schema_versions_info = [
+ {'min': None, 'max': '2.1', 'schema': schemav21},
+ {'min': '2.2', 'max': '2.9', 'schema': schemav22},
+ {'min': '2.10', 'max': None, 'schema': schemav210}]
"""
schema = None
version = api_version_request.APIVersionRequest(COMPUTE_MICROVERSION)
diff --git a/tempest/lib/services/identity/v2/token_client.py b/tempest/lib/services/identity/v2/token_client.py
index 0350175..5716027 100644
--- a/tempest/lib/services/identity/v2/token_client.py
+++ b/tempest/lib/services/identity/v2/token_client.py
@@ -75,8 +75,12 @@
return rest_client.ResponseBody(resp, body['access'])
def request(self, method, url, extra_headers=False, headers=None,
- body=None):
- """A simple HTTP request interface."""
+ body=None, chunked=False):
+ """A simple HTTP request interface.
+
+ Note: this overloads the `request` method from the parent class and
+ thus must implement the same method signature.
+ """
if headers is None:
headers = self.get_headers(accept_type="json")
elif extra_headers:
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index f342a49..964d43f 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -122,8 +122,12 @@
return rest_client.ResponseBody(resp, body)
def request(self, method, url, extra_headers=False, headers=None,
- body=None):
- """A simple HTTP request interface."""
+ body=None, chunked=False):
+ """A simple HTTP request interface.
+
+ Note: this overloads the `request` method from the parent class and
+ thus must implement the same method signature.
+ """
if headers is None:
# Always accept 'json', for xml token client too.
# Because XML response is not easily
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 956fe88..7389722 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -459,13 +459,13 @@
LOG.debug("Creating a snapshot image for server: %s", server['name'])
image = _images_client.create_image(server['id'], name=name)
image_id = image.response['location'].split('images/')[1]
- _image_client.wait_for_image_status(image_id, 'active')
+ waiters.wait_for_image_status(_image_client, image_id, 'active')
self.addCleanup_with_wait(
waiter_callable=_image_client.wait_for_resource_deletion,
thing_id=image_id, thing_id_param='id',
cleanup_callable=self.delete_wrapper,
cleanup_args=[_image_client.delete_image, image_id])
- snapshot_image = _image_client.get_image_meta(image_id)
+ snapshot_image = _image_client.check_image(image_id)
bdm = snapshot_image.get('properties', {}).get('block_device_mapping')
if bdm:
@@ -1069,10 +1069,11 @@
security_groups_client=None):
"""Create loginable security group rule
- These rules are intended to permit inbound ssh and icmp
- traffic from all sources, so no group_id is provided.
- Setting a group_id would only permit traffic from ports
- belonging to the same security group.
+ This function will create:
+ 1. egress and ingress tcp port 22 allow rule in order to allow ssh
+ access for ipv4.
+ 2. egress and ingress ipv6 icmp allow rule, in order to allow icmpv6.
+ 3. egress and ingress ipv4 icmp allow rule, in order to allow icmpv4.
"""
if security_group_rules_client is None:
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index b9fdd18..f0ae223 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -754,10 +754,10 @@
The test steps are :
1. Create a new network.
2. Connect (hotplug) the VM to a new network.
- 3. Check the VM can ping the DHCP interface of this network.
+ 3. Check the VM can ping a server on the new network ("peer")
4. Spoof the mac address of the new VM interface.
5. Check the Security Group enforces mac spoofing and blocks pings via
- spoofed interface (VM cannot ping the DHCP interface).
+ spoofed interface (VM cannot ping the peer).
6. Disable port-security of the spoofed port- set the flag to false.
7. Retest 3rd step and check that the Security Group allows pings via
the spoofed interface.
@@ -778,18 +778,18 @@
ssh_client = self.get_remote_client(fip.floating_ip_address,
private_key=private_key)
spoof_nic = ssh_client.get_nic_name_by_mac(spoof_port["mac_address"])
- dhcp_ports = self._list_ports(device_owner="network:dhcp",
- network_id=self.new_net["id"])
- new_net_dhcp = dhcp_ports[0]["fixed_ips"][0]["ip_address"]
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
+ name = data_utils.rand_name('peer')
+ peer = self._create_server(name, self.new_net)
+ peer_address = peer['addresses'][self.new_net.name][0]['addr']
+ self._check_remote_connectivity(ssh_client, dest=peer_address,
nic=spoof_nic, should_succeed=True)
ssh_client.set_mac_address(spoof_nic, spoof_mac)
new_mac = ssh_client.get_mac_address(nic=spoof_nic)
self.assertEqual(spoof_mac, new_mac)
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
+ self._check_remote_connectivity(ssh_client, dest=peer_address,
nic=spoof_nic, should_succeed=False)
self.ports_client.update_port(spoof_port["id"],
port_security_enabled=False,
security_groups=[])
- self._check_remote_connectivity(ssh_client, dest=new_net_dhcp,
+ self._check_remote_connectivity(ssh_client, dest=peer_address,
nic=spoof_nic, should_succeed=True)
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
index 6e24801..2bdd092 100644
--- a/tempest/services/baremetal/base.py
+++ b/tempest/services/baremetal/base.py
@@ -111,7 +111,7 @@
uri += "?%s" % urllib.urlencode(kwargs)
resp, body = self.get(uri)
- self.expected_success(200, resp['status'])
+ self.expected_success(200, resp.status)
return resp, self.deserialize(body)
@@ -127,7 +127,7 @@
else:
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
resp, body = self.get(uri)
- self.expected_success(200, resp['status'])
+ self.expected_success(200, resp.status)
return resp, self.deserialize(body)
@@ -145,7 +145,7 @@
uri = self._get_uri(resource)
resp, body = self.post(uri, body=body)
- self.expected_success(201, resp['status'])
+ self.expected_success(201, resp.status)
return resp, self.deserialize(body)
@@ -160,7 +160,7 @@
uri = self._get_uri(resource, uuid)
resp, body = self.delete(uri)
- self.expected_success(204, resp['status'])
+ self.expected_success(204, resp.status)
return resp, body
def _patch_request(self, resource, uuid, patch_object):
@@ -176,7 +176,7 @@
patch_body = json.dumps(patch_object)
resp, body = self.patch(uri, body=patch_body)
- self.expected_success(200, resp['status'])
+ self.expected_success(200, resp.status)
return resp, self.deserialize(body)
@handle_errors
@@ -201,5 +201,5 @@
put_body = json.dumps(put_object)
resp, body = self.put(uri, body=put_body)
- self.expected_success(202, resp['status'])
+ self.expected_success([202, 204], resp.status)
return resp, body
diff --git a/tempest/services/image/v1/json/images_client.py b/tempest/services/image/v1/json/images_client.py
index e29ff89..a36075f 100644
--- a/tempest/services/image/v1/json/images_client.py
+++ b/tempest/services/image/v1/json/images_client.py
@@ -16,7 +16,6 @@
import copy
import errno
import os
-import time
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
@@ -24,9 +23,7 @@
from six.moves.urllib import parse as urllib
from tempest.common import glance_http
-from tempest import exceptions
from tempest.lib.common import rest_client
-from tempest.lib.common.utils import misc as misc_utils
from tempest.lib import exceptions as lib_exc
LOG = logging.getLogger(__name__)
@@ -198,7 +195,8 @@
body = json.loads(body)
return rest_client.ResponseBody(resp, body)
- def get_image_meta(self, image_id):
+ def check_image(self, image_id):
+ """Check image metadata."""
url = 'v1/images/%s' % image_id
resp, __ = self.head(url)
self.expected_success(200, resp.status)
@@ -206,6 +204,7 @@
return rest_client.ResponseBody(resp, body)
def show_image(self, image_id):
+ """Get image details plus the image itself."""
url = 'v1/images/%s' % image_id
resp, body = self.get(url)
self.expected_success(200, resp.status)
@@ -213,7 +212,7 @@
def is_resource_deleted(self, id):
try:
- if self.get_image_meta(id)['status'] == 'deleted':
+ if self.check_image(id)['status'] == 'deleted':
return True
except lib_exc.NotFound:
return True
@@ -256,40 +255,3 @@
resp, __ = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp)
-
- # NOTE(afazekas): just for the wait function
- def _get_image_status(self, image_id):
- meta = self.get_image_meta(image_id)
- status = meta['status']
- return status
-
- # NOTE(afazkas): Wait reinvented again. It is not in the correct layer
- def wait_for_image_status(self, image_id, status):
- """Waits for a Image to reach a given status."""
- start_time = time.time()
- old_value = value = self._get_image_status(image_id)
- while True:
- dtime = time.time() - start_time
- time.sleep(self.build_interval)
- if value != old_value:
- LOG.info('Value transition from "%s" to "%s"'
- 'in %d second(s).', old_value,
- value, dtime)
- if value == status:
- return value
-
- if value == 'killed':
- raise exceptions.ImageKilledException(image_id=image_id,
- status=status)
- if dtime > self.build_timeout:
- message = ('Time Limit Exceeded! (%ds)'
- 'while waiting for %s, '
- 'but we got %s.' %
- (self.build_timeout, status, value))
- caller = misc_utils.find_test_caller()
- if caller:
- message = '(%s) %s' % (caller, message)
- raise exceptions.TimeoutException(message)
- time.sleep(self.build_interval)
- old_value = value
- value = self._get_image_status(image_id)
diff --git a/tempest/services/image/v2/json/images_client.py b/tempest/services/image/v2/json/images_client.py
index 4e037af..a61f31d 100644
--- a/tempest/services/image/v2/json/images_client.py
+++ b/tempest/services/image/v2/json/images_client.py
@@ -21,10 +21,10 @@
from tempest.lib import exceptions as lib_exc
-class ImagesClientV2(rest_client.RestClient):
+class ImagesClient(rest_client.RestClient):
def __init__(self, auth_provider, catalog_type, region, **kwargs):
- super(ImagesClientV2, self).__init__(
+ super(ImagesClient, self).__init__(
auth_provider, catalog_type, region, **kwargs)
self._http = None
self.dscv = kwargs.get("disable_ssl_certificate_validation")
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index fa43d94..33dba6e 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -149,25 +149,30 @@
self.expected_success(201, resp.status)
return resp, body
- def put_object_with_chunk(self, container, name, contents, chunk_size):
- """Put an object with Transfer-Encoding header"""
+ def put_object_with_chunk(self, container, name, contents):
+ """Put an object with Transfer-Encoding header
+
+ :param container: name of the container
+ :type container: string
+ :param name: name of the object
+ :type name: string
+ :param contents: object data
+ :type contents: iterable
+ """
headers = {'Transfer-Encoding': 'chunked'}
if self.token:
headers['X-Auth-Token'] = self.token
- conn = put_object_connection(self.base_url, container, name, contents,
- chunk_size, headers)
-
- resp = conn.getresponse()
- body = resp.read()
-
- resp_headers = {}
- for header, value in resp.getheaders():
- resp_headers[header.lower()] = value
+ url = "%s/%s" % (container, name)
+ resp, body = self.put(
+ url, headers=headers,
+ body=contents,
+ chunked=True
+ )
self._error_checker('PUT', None, headers, contents, resp, body)
self.expected_success(201, resp.status)
- return resp.status, resp.reason, resp_headers
+ return resp.status, resp.reason, resp
def create_object_continue(self, container, object_name,
data, metadata=None):
@@ -262,30 +267,7 @@
headers = dict(headers)
else:
headers = {}
- if hasattr(contents, 'read'):
- conn.putrequest('PUT', path)
- for header, value in six.iteritems(headers):
- conn.putheader(header, value)
- if 'Content-Length' not in headers:
- if 'Transfer-Encoding' not in headers:
- conn.putheader('Transfer-Encoding', 'chunked')
- conn.endheaders()
- chunk = contents.read(chunk_size)
- while chunk:
- conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
- chunk = contents.read(chunk_size)
- conn.send('0\r\n\r\n')
- else:
- conn.endheaders()
- left = headers['Content-Length']
- while left > 0:
- size = chunk_size
- if size > left:
- size = left
- chunk = contents.read(size)
- conn.send(chunk)
- left -= len(chunk)
- else:
- conn.request('PUT', path, contents, headers)
+
+ conn.request('PUT', path, contents, headers)
return conn
diff --git a/tempest/stress/driver.py b/tempest/stress/driver.py
index 382b851..2beaaa9 100644
--- a/tempest/stress/driver.py
+++ b/tempest/stress/driver.py
@@ -135,10 +135,13 @@
break
if skip:
break
+ # TODO(andreaf) This has to be reworked to use the credential
+ # provider interface. For now only tests marked as 'use_admin' will
+ # work.
if test.get('use_admin', False):
manager = admin_manager
else:
- manager = credentials.ConfiguredUserManager()
+ raise NotImplemented('Non admin tests are not supported')
for p_number in moves.xrange(test.get('threads', default_thread_num)):
if test.get('use_isolated_tenants', False):
username = data_utils.rand_name("stress_user")
diff --git a/tempest/tests/common/test_preprov_creds.py b/tempest/tests/common/test_preprov_creds.py
index b595c88..873c4c4 100644
--- a/tempest/tests/common/test_preprov_creds.py
+++ b/tempest/tests/common/test_preprov_creds.py
@@ -27,7 +27,6 @@
from tempest import config
from tempest.lib import auth
from tempest.lib import exceptions as lib_exc
-from tempest.lib.services.identity.v2 import token_client
from tempest.tests import base
from tempest.tests import fake_config
from tempest.tests.lib import fake_identity
@@ -43,40 +42,46 @@
'object_storage_operator_role': 'operator',
'object_storage_reseller_admin_role': 'reseller'}
+ identity_response = fake_identity._fake_v2_response
+ token_client = ('tempest.lib.services.identity.v2.token_client'
+ '.TokenClient.raw_request')
+
+ @classmethod
+ def _fake_accounts(cls, admin_role):
+ return [
+ {'username': 'test_user1', 'tenant_name': 'test_tenant1',
+ 'password': 'p'},
+ {'username': 'test_user2', 'project_name': 'test_tenant2',
+ 'password': 'p'},
+ {'username': 'test_user3', 'tenant_name': 'test_tenant3',
+ 'password': 'p'},
+ {'username': 'test_user4', 'project_name': 'test_tenant4',
+ 'password': 'p'},
+ {'username': 'test_user5', 'tenant_name': 'test_tenant5',
+ 'password': 'p'},
+ {'username': 'test_user6', 'project_name': 'test_tenant6',
+ 'password': 'p', 'roles': ['role1', 'role2']},
+ {'username': 'test_user7', 'tenant_name': 'test_tenant7',
+ 'password': 'p', 'roles': ['role2', 'role3']},
+ {'username': 'test_user8', 'project_name': 'test_tenant8',
+ 'password': 'p', 'roles': ['role4', 'role1']},
+ {'username': 'test_user9', 'tenant_name': 'test_tenant9',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user10', 'project_name': 'test_tenant10',
+ 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user11', 'tenant_name': 'test_tenant11',
+ 'password': 'p', 'roles': [admin_role]},
+ {'username': 'test_user12', 'project_name': 'test_tenant12',
+ 'password': 'p', 'roles': [admin_role]}]
+
def setUp(self):
super(TestPreProvisionedCredentials, self).setUp()
self.useFixture(fake_config.ConfigFixture())
self.patchobject(config, 'TempestConfigPrivate',
fake_config.FakePrivate)
- self.patchobject(token_client.TokenClient, 'raw_request',
- fake_identity._fake_v2_response)
+ self.patch(self.token_client, side_effect=self.identity_response)
self.useFixture(lockutils_fixtures.ExternalLockFixture())
- self.test_accounts = [
- {'username': 'test_user1', 'tenant_name': 'test_tenant1',
- 'password': 'p'},
- {'username': 'test_user2', 'tenant_name': 'test_tenant2',
- 'password': 'p'},
- {'username': 'test_user3', 'tenant_name': 'test_tenant3',
- 'password': 'p'},
- {'username': 'test_user4', 'tenant_name': 'test_tenant4',
- 'password': 'p'},
- {'username': 'test_user5', 'tenant_name': 'test_tenant5',
- 'password': 'p'},
- {'username': 'test_user6', 'tenant_name': 'test_tenant6',
- 'password': 'p', 'roles': ['role1', 'role2']},
- {'username': 'test_user7', 'tenant_name': 'test_tenant7',
- 'password': 'p', 'roles': ['role2', 'role3']},
- {'username': 'test_user8', 'tenant_name': 'test_tenant8',
- 'password': 'p', 'roles': ['role4', 'role1']},
- {'username': 'test_user9', 'tenant_name': 'test_tenant9',
- 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
- {'username': 'test_user10', 'tenant_name': 'test_tenant10',
- 'password': 'p', 'roles': ['role1', 'role2', 'role3', 'role4']},
- {'username': 'test_user11', 'tenant_name': 'test_tenant11',
- 'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
- {'username': 'test_user12', 'tenant_name': 'test_tenant12',
- 'password': 'p', 'roles': [cfg.CONF.identity.admin_role]},
- ]
+ self.test_accounts = self._fake_accounts(cfg.CONF.identity.admin_role)
self.accounts_mock = self.useFixture(mockpatch.Patch(
'tempest.common.preprov_creds.read_accounts_yaml',
return_value=self.test_accounts))
@@ -89,24 +94,33 @@
def _get_hash_list(self, accounts_list):
hash_list = []
+ hash_fields = (
+ preprov_creds.PreProvisionedCredentialProvider.HASH_CRED_FIELDS)
for account in accounts_list:
hash = hashlib.md5()
- hash.update(six.text_type(account).encode('utf-8'))
+ account_for_hash = dict((k, v) for (k, v) in six.iteritems(account)
+ if k in hash_fields)
+ hash.update(six.text_type(account_for_hash).encode('utf-8'))
temp_hash = hash.hexdigest()
hash_list.append(temp_hash)
return hash_list
def test_get_hash(self):
- self.patchobject(token_client.TokenClient, 'raw_request',
- fake_identity._fake_v2_response)
- test_account_class = preprov_creds.PreProvisionedCredentialProvider(
- **self.fixed_params)
- hash_list = self._get_hash_list(self.test_accounts)
- test_cred_dict = self.test_accounts[3]
- test_creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL,
- **test_cred_dict)
- results = test_account_class.get_hash(test_creds)
- self.assertEqual(hash_list[3], results)
+ # Test with all accounts to make sure we try all combinations
+ # and hide no race conditions
+ hash_index = 0
+ for test_cred_dict in self.test_accounts:
+ test_account_class = (
+ preprov_creds.PreProvisionedCredentialProvider(
+ **self.fixed_params))
+ hash_list = self._get_hash_list(self.test_accounts)
+ test_creds = auth.get_credentials(
+ fake_identity.FAKE_AUTH_URL,
+ identity_version=self.fixed_params['identity_version'],
+ **test_cred_dict)
+ results = test_account_class.get_hash(test_creds)
+ self.assertEqual(hash_list[hash_index], results)
+ hash_index += 1
def test_get_hash_dict(self):
test_account_class = preprov_creds.PreProvisionedCredentialProvider(
@@ -331,3 +345,53 @@
self.assertIn('id', network)
self.assertEqual('fake-id', network['id'])
self.assertEqual('network-2', network['name'])
+
+
+class TestPreProvisionedCredentialsV3(TestPreProvisionedCredentials):
+
+ fixed_params = {'name': 'test class',
+ 'identity_version': 'v3',
+ 'test_accounts_file': 'fake_accounts_file',
+ 'accounts_lock_dir': 'fake_locks_dir_v3',
+ 'admin_role': 'admin',
+ 'object_storage_operator_role': 'operator',
+ 'object_storage_reseller_admin_role': 'reseller'}
+
+ identity_response = fake_identity._fake_v3_response
+ token_client = ('tempest.lib.services.identity.v3.token_client'
+ '.V3TokenClient.raw_request')
+
+ @classmethod
+ def _fake_accounts(cls, admin_role):
+ return [
+ {'username': 'test_user1', 'project_name': 'test_project1',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user2', 'project_name': 'test_project2',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user3', 'project_name': 'test_project3',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user4', 'project_name': 'test_project4',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user5', 'project_name': 'test_project5',
+ 'domain_name': 'domain', 'password': 'p'},
+ {'username': 'test_user6', 'project_name': 'test_project6',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2']},
+ {'username': 'test_user7', 'project_name': 'test_project7',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role2', 'role3']},
+ {'username': 'test_user8', 'project_name': 'test_project8',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role4', 'role1']},
+ {'username': 'test_user9', 'project_name': 'test_project9',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user10', 'project_name': 'test_project10',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': ['role1', 'role2', 'role3', 'role4']},
+ {'username': 'test_user11', 'project_name': 'test_project11',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': [admin_role]},
+ {'username': 'test_user12', 'project_name': 'test_project12',
+ 'domain_name': 'domain', 'password': 'p',
+ 'roles': [admin_role]}]
diff --git a/tempest/tests/common/test_waiters.py b/tempest/tests/common/test_waiters.py
index 492bdca..a56f837 100644
--- a/tempest/tests/common/test_waiters.py
+++ b/tempest/tests/common/test_waiters.py
@@ -38,8 +38,7 @@
# Ensure waiter returns before build_timeout
self.assertTrue((end_time - start_time) < 10)
- @mock.patch('time.sleep')
- def test_wait_for_image_status_timeout(self, mock_sleep):
+ def test_wait_for_image_status_timeout(self):
time_mock = self.patch('time.time')
time_mock.side_effect = utils.generate_timeout_series(1)
@@ -47,15 +46,12 @@
self.assertRaises(exceptions.TimeoutException,
waiters.wait_for_image_status,
self.client, 'fake_image_id', 'active')
- mock_sleep.assert_called_once_with(1)
- @mock.patch('time.sleep')
- def test_wait_for_image_status_error_on_image_create(self, mock_sleep):
+ def test_wait_for_image_status_error_on_image_create(self):
self.client.show_image.return_value = ({'status': 'ERROR'})
self.assertRaises(exceptions.AddImageException,
waiters.wait_for_image_status,
self.client, 'fake_image_id', 'active')
- mock_sleep.assert_called_once_with(1)
@mock.patch.object(time, 'sleep')
def test_wait_for_volume_status_error_restoring(self, mock_sleep):
diff --git a/tempest/tests/common/utils/test_file_utils.py b/tempest/tests/common/utils/test_file_utils.py
deleted file mode 100644
index 937aefa..0000000
--- a/tempest/tests/common/utils/test_file_utils.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright 2014 IBM Corp.
-# 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
-
-from tempest.common.utils import file_utils
-from tempest.tests import base
-
-
-class TestFileUtils(base.TestCase):
-
- def test_have_effective_read_path(self):
- with mock.patch('six.moves.builtins.open', mock.mock_open(),
- create=True):
- result = file_utils.have_effective_read_access('fake_path')
- self.assertTrue(result)
-
- def test_not_effective_read_path(self):
- result = file_utils.have_effective_read_access('fake_path')
- self.assertFalse(result)
diff --git a/tempest/tests/lib/common/utils/test_data_utils.py b/tempest/tests/lib/common/utils/test_data_utils.py
index f9e1f44..399c4af 100644
--- a/tempest/tests/lib/common/utils/test_data_utils.py
+++ b/tempest/tests/lib/common/utils/test_data_utils.py
@@ -169,3 +169,10 @@
bad_mac = 99999999999999999999
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
cidr, bad_mac)
+
+ def test_chunkify(self):
+ data = "aaa"
+ chunks = data_utils.chunkify(data, 2)
+ self.assertEqual("aa", next(chunks))
+ self.assertEqual("a", next(chunks))
+ self.assertRaises(StopIteration, next, chunks)
diff --git a/tempest/tests/lib/fake_http.py b/tempest/tests/lib/fake_http.py
index 397c856..cfa4b93 100644
--- a/tempest/tests/lib/fake_http.py
+++ b/tempest/tests/lib/fake_http.py
@@ -21,7 +21,7 @@
self.return_type = return_type
def request(self, uri, method="GET", body=None, headers=None,
- redirections=5, connection_type=None):
+ redirections=5, connection_type=None, chunked=False):
if not self.return_type:
fake_headers = fake_http_response(headers)
return_obj = {
diff --git a/tempest/tests/lib/test_credentials.py b/tempest/tests/lib/test_credentials.py
index ca3baa1..b6f2cf6 100644
--- a/tempest/tests/lib/test_credentials.py
+++ b/tempest/tests/lib/test_credentials.py
@@ -36,8 +36,10 @@
# Check the right version of credentials has been returned
self.assertIsInstance(credentials, credentials_class)
# Check the id attributes are filled in
+ # NOTE(andreaf) project_* attributes are accepted as input but
+ # never set on the credentials object
attributes = [x for x in credentials.ATTRIBUTES if (
- '_id' in x and x != 'domain_id')]
+ '_id' in x and x != 'domain_id' and x != 'project_id')]
for attr in attributes:
if filled:
self.assertIsNotNone(getattr(credentials, attr))
diff --git a/test-requirements.txt b/test-requirements.txt
index 9ef956a..763f0ba 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,6 +7,6 @@
python-subunit>=0.0.18 # Apache-2.0/BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
reno>=1.6.2 # Apache2
-mock>=1.2 # BSD
+mock>=2.0 # BSD
coverage>=3.6 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0