Merge "Add XML support for extensions_client"
diff --git a/.gitignore b/.gitignore
index 9355e87..e5106f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@
include/swift_objects/swift_large
*.log
*.swp
+*.egg-info
+.tox
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index ecd1e2a..8d3f6c9 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -33,6 +33,12 @@
# are known.
allow_tenant_isolation = true
+# Allows test cases to create/destroy tenants and users. This option
+# enables isolated test cases and better parallel execution,
+# but also requires that OpenStack Identity API admin credentials
+# are known.
+allow_tenant_reuse = true
+
# This should be the username of a user WITHOUT administrative privileges
username = demo
# The above non-administrative user's password
@@ -56,10 +62,10 @@
flavor_ref_alt = 2
# Number of seconds to wait while looping to check the status of an
-# instance or volume that is building.
+# instance that is building.
build_interval = 10
-# Number of seconds to time out on waiting for an instance or volume
+# Number of seconds to time out on waiting for an instance
# to build or reach an expected status
build_timeout = 600
@@ -119,7 +125,7 @@
path_to_private_key = /home/user/.ssh/id_rsa
# Connection string to the database of Compute service
-db_uri = mysql:///user:pass@localhost/nova
+db_uri = mysql://user:pass@localhost/nova
[image]
# This section contains configuration options used when executing tests
@@ -161,7 +167,10 @@
[network]
# This section contains configuration options used when executing tests
# against the OpenStack Network API.
+
+# Version of the Quantum API
api_version = v1.1
+# Catalog type of the Quantum Service
catalog_type = network
[identity-admin]
@@ -175,3 +184,18 @@
password = pass
# The above administrative user's tenant name
tenant_name = admin
+
+[volume]
+# This section contains the configuration options used when executng tests
+# against the OpenStack Block Storage API service
+
+# The type of endpoint for a Cinder or Block Storage API service.
+# Unless you have a custom Keystone service catalog implementation, you
+# probably want to leave this value as "volume"
+catalog_type = volume
+# Number of seconds to wait while looping to check the status of a
+# volume that is being made available
+build_interval = 10
+# Number of seconds to time out on waiting for a volume
+# to be available or reach an expected status
+build_timeout = 300
diff --git a/etc/tempest.conf.tpl b/etc/tempest.conf.tpl
index f268de2..ecb020a 100644
--- a/etc/tempest.conf.tpl
+++ b/etc/tempest.conf.tpl
@@ -29,6 +29,12 @@
# are known.
allow_tenant_isolation = %COMPUTE_ALLOW_TENANT_ISOLATION%
+# Allows test cases to create/destroy tenants and users. This option
+# enables isolated test cases and better parallel execution,
+# but also requires that OpenStack Identity API admin credentials
+# are known.
+allow_tenant_reuse = %COMPUTE_ALLOW_TENANT_REUSE%
+
# This should be the username of a user WITHOUT administrative privileges
username = %USERNAME%
# The above non-administrative user's password
@@ -52,12 +58,12 @@
flavor_ref_alt = %FLAVOR_REF_ALT%
# Number of seconds to wait while looping to check the status of an
-# instance or volume that is building.
-build_interval = %BUILD_INTERVAL%
+# instance that is building.
+build_interval = %COMPUTE_BUILD_INTERVAL%
-# Number of seconds to time out on waiting for an instance or volume
+# Number of seconds to time out on waiting for an instance
# to build or reach an expected status
-build_timeout = %BUILD_TIMEOUT%
+build_timeout = %COMPUTE_BUILD_TIMEOUT%
# The type of endpoint for a Compute API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
@@ -148,3 +154,18 @@
password = %IDENTITY_ADMIN_PASSWORD%
# The above administrative user's tenant name
tenant_name = %IDENTITY_ADMIN_TENANT_NAME%
+
+[volume]
+# This section contains the configuration options used when executing tests
+# against the OpenStack Block Storage API service
+
+# The type of endpoint for a Cinder or Block Storage API service.
+# Unless you have a custom Keystone service catalog implementation, you
+# probably want to leave this value as "volume"
+catalog_type = %VOLUME_CATALOG_TYPE%
+# Number of seconds to wait while looping to check the status of a
+# volume that is being made available
+build_interval = %VOLUME_BUILD_INTERVAL%
+# Number of seconds to time out on waiting for a volume
+# to be available or reach an expected status
+build_timeout = %VOLUME_BUILD_TIMEOUT%
diff --git a/tempest/config.py b/tempest/config.py
index 28a0c5e..38455b2 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -125,6 +125,17 @@
return self.get("allow_tenant_isolation", 'false').lower() != 'false'
@property
+ def allow_tenant_reuse(self):
+ """
+ If allow_tenant_isolation is True and a tenant that would be created
+ for a given test already exists (such as from a previously-failed run),
+ re-use that tenant instead of failing because of the conflict. Note
+ that this would result in the tenant being deleted at the end of a
+ subsequent successful run.
+ """
+ return self.get("allow_tenant_reuse", 'true').lower() != 'false'
+
+ @property
def username(self):
"""Username to use for Nova API requests."""
return self.get("username", "demo")
@@ -197,7 +208,7 @@
@property
def build_timeout(self):
- """Timeout in seconds to wait for an entity to build."""
+ """Timeout in seconds to wait for an instance to build."""
return float(self.get("build_timeout", 300))
@property
@@ -344,6 +355,30 @@
return self.get("api_version", "v1.1")
+class VolumeConfig(BaseConfig):
+ """Provides configuration information for connecting to an OpenStack Block
+ Storage Service.
+ """
+
+ SECTION_NAME = "volume"
+
+ @property
+ def build_interval(self):
+ """Time in seconds between volume availability checks."""
+ return float(self.get("build_interval", 10))
+
+ @property
+ def build_timeout(self):
+ """Timeout in seconds to wait for a volume to become available."""
+ return float(self.get("build_timeout", 300))
+
+ @property
+ def catalog_type(self):
+ """Catalog type of the Volume Service"""
+ return self.get("catalog_type", 'volume')
+
+
+# TODO(jaypipes): Move this to a common utils (not data_utils...)
def singleton(cls):
"""Simple wrapper for classes that should only have a single instance"""
instances = {}
@@ -391,6 +426,7 @@
self.identity_admin = IdentityAdminConfig(self._conf)
self.images = ImagesConfig(self._conf)
self.network = NetworkConfig(self._conf)
+ self.volume = VolumeConfig(self._conf)
def load_config(self, path):
"""Read configuration from given path and return a config object."""
diff --git a/tempest/manager.py b/tempest/manager.py
index 0c74439..228c3b9 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -18,13 +18,16 @@
import logging
# Default client libs
+import glanceclient
+import keystoneclient.v2_0.client
import novaclient.client
-import glance.client
+import quantumclient.v2_0.client
import tempest.config
from tempest import exceptions
# Tempest REST Fuzz testing client libs
from tempest.services.network.json import network_client
+from tempest.services.volume.json import volumes_client
from tempest.services.nova.json import images_client
from tempest.services.nova.json import flavors_client
from tempest.services.nova.json import servers_client
@@ -33,7 +36,7 @@
from tempest.services.nova.json import security_groups_client
from tempest.services.nova.json import floating_ips_client
from tempest.services.nova.json import keypairs_client
-from tempest.services.nova.json import volumes_client
+from tempest.services.nova.json import volumes_extensions_client
from tempest.services.nova.json import console_output_client
NetworkClient = network_client.NetworkClient
@@ -45,6 +48,7 @@
SecurityGroupsClient = security_groups_client.SecurityGroupsClient
FloatingIPsClient = floating_ips_client.FloatingIPsClient
KeyPairsClient = keypairs_client.KeyPairsClientJSON
+VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClient
VolumesClient = volumes_client.VolumesClient
ConsoleOutputsClient = console_output_client.ConsoleOutputsClient
@@ -62,16 +66,7 @@
def __init__(self):
self.config = tempest.config.TempestConfig()
- self.client = None
-
-
-class DefaultClientManager(Manager):
-
- """
- Manager class that indicates the client provided by the manager
- is the default Python client that an OpenStack API provides.
- """
- pass
+ self.client_attr_names = []
class FuzzClientManager(Manager):
@@ -86,23 +81,42 @@
pass
-class ComputeDefaultClientManager(DefaultClientManager):
+class DefaultClientManager(Manager):
"""
- Manager that provides the default python-novaclient client object
- to access the OpenStack Compute API.
+ Manager that provides the default clients to access the various
+ OpenStack APIs.
"""
NOVACLIENT_VERSION = '2'
def __init__(self):
- super(ComputeDefaultClientManager, self).__init__()
- username = self.config.compute.username
- password = self.config.compute.password
- tenant_name = self.config.compute.tenant_name
+ super(DefaultClientManager, self).__init__()
+ self.compute_client = self._get_compute_client()
+ self.image_client = self._get_image_client()
+ self.identity_client = self._get_identity_client()
+ self.network_client = self._get_network_client()
+ self.client_attr_names = [
+ 'compute_client',
+ 'image_client',
+ 'identity_client',
+ 'network_client',
+ ]
+
+ def _get_compute_client(self, username=None, password=None,
+ tenant_name=None):
+ # Novaclient will not execute operations for anyone but the
+ # identified user, so a new client needs to be created for
+ # each user that operations need to be performed for.
+ if not username:
+ username = self.config.compute.username
+ if not password:
+ password = self.config.compute.password
+ if not tenant_name:
+ tenant_name = self.config.compute.tenant_name
if None in (username, password, tenant_name):
- msg = ("Missing required credentials. "
+ msg = ("Missing required credentials for compute client. "
"username: %(username)s, password: %(password)s, "
"tenant_name: %(tenant_name)s") % locals()
raise exceptions.InvalidConfiguration(msg)
@@ -113,43 +127,56 @@
client_args = (username, password, tenant_name, auth_url)
# Create our default Nova client to use in testing
- self.client = novaclient.client.Client(self.NOVACLIENT_VERSION,
+ return novaclient.client.Client(self.NOVACLIENT_VERSION,
*client_args,
service_type=self.config.compute.catalog_type,
no_cache=True)
+ def _get_image_client(self):
+ keystone = self._get_identity_client()
+ token = keystone.auth_token
+ endpoint = keystone.service_catalog.url_for(service_type='image',
+ endpoint_type='publicURL')
+ return glanceclient.Client('1', endpoint=endpoint, token=token)
-class GlanceDefaultClientManager(DefaultClientManager):
- """
- Manager that provides the default glance client object to access
- the OpenStack Images API
- """
- def __init__(self):
- super(GlanceDefaultClientManager, self).__init__()
- host = self.config.images.host
- port = self.config.images.port
- strategy = self.config.identity.strategy
- auth_url = self.config.identity.auth_url
- username = self.config.images.username
- password = self.config.images.password
- tenant_name = self.config.images.tenant_name
+ def _get_identity_client(self):
+ # This identity client is not intended to check the security
+ # of the identity service, so use admin credentials.
+ username = self.config.identity_admin.username
+ password = self.config.identity_admin.password
+ tenant_name = self.config.identity_admin.tenant_name
- if None in (host, port, username, password, tenant_name):
- msg = ("Missing required credentials. "
- "host:%(host)s, port: %(port)s username: %(username)s, "
- "password: %(password)s, "
- "tenant_name: %(tenant_name)s") % locals()
+ if None in (username, password, tenant_name):
+ msg = ("Missing required credentials for identity client. "
+ "username: %(username)s, password: %(password)s, "
+ "tenant_name: %(tenant_name)s") % locals()
raise exceptions.InvalidConfiguration(msg)
+
auth_url = self.config.identity.auth_url.rstrip('tokens')
- creds = {'strategy': strategy,
- 'username': username,
- 'password': password,
- 'tenant': tenant_name,
- 'auth_url': auth_url}
+ return keystoneclient.v2_0.client.Client(username=username,
+ password=password,
+ tenant_name=tenant_name,
+ endpoint=auth_url)
- # Create our default Glance client to use in testing
- self.client = glance.client.Client(host, port, creds=creds)
+ def _get_network_client(self):
+ # TODO(mnewby) add network-specific auth configuration
+ username = self.config.compute.username
+ password = self.config.compute.password
+ tenant_name = self.config.compute.tenant_name
+
+ if None in (username, password, tenant_name):
+ msg = ("Missing required credentials for network client. "
+ "username: %(username)s, password: %(password)s, "
+ "tenant_name: %(tenant_name)s") % locals()
+ raise exceptions.InvalidConfiguration(msg)
+
+ auth_url = self.config.identity.auth_url.rstrip('tokens')
+
+ return quantumclient.v2_0.client.Client(username=username,
+ password=password,
+ tenant_name=tenant_name,
+ auth_url=auth_url)
class ComputeFuzzClientManager(FuzzClientManager):
@@ -199,6 +226,7 @@
self.keypairs_client = KeyPairsClient(*client_args)
self.security_groups_client = SecurityGroupsClient(*client_args)
self.floating_ips_client = FloatingIPsClient(*client_args)
+ self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
self.volumes_client = VolumesClient(*client_args)
self.console_outputs_client = ConsoleOutputsClient(*client_args)
self.network_client = NetworkClient(*client_args)
diff --git a/tempest/openstack.py b/tempest/openstack.py
index 88cb80c..27ae6c0 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -23,6 +23,7 @@
from tempest.services.network.json.network_client import NetworkClient
from tempest.services.nova.json.extensions_client import ExtensionsClientJSON
from tempest.services.nova.json.flavors_client import FlavorsClientJSON
+from tempest.services.volume.json.volumes_client import VolumesClient
from tempest.services.nova.json.images_client import ImagesClient
from tempest.services.nova.json.limits_client import LimitsClientJSON
from tempest.services.nova.json.servers_client import ServersClientJSON
@@ -30,7 +31,8 @@
import SecurityGroupsClient
from tempest.services.nova.json.floating_ips_client import FloatingIPsClient
from tempest.services.nova.json.keypairs_client import KeyPairsClientJSON
-from tempest.services.nova.json.volumes_client import VolumesClient
+from tempest.services.nova.json.volumes_extensions_client \
+import VolumesExtensionsClient
from tempest.services.nova.json.console_output_client \
import ConsoleOutputsClient
from tempest.services.nova.xml.extensions_client import ExtensionsClientXML
@@ -88,23 +90,24 @@
# If no creds are provided, we fall back on the defaults
# in the config file for the Compute API.
- username = username or self.config.compute.username
- password = password or self.config.compute.password
- tenant_name = tenant_name or self.config.compute.tenant_name
+ self.username = username or self.config.compute.username
+ self.password = password or self.config.compute.password
+ self.tenant_name = tenant_name or self.config.compute.tenant_name
- if None in (username, password, tenant_name):
+ if None in (self.username, self.password, self.tenant_name):
msg = ("Missing required credentials. "
"username: %(username)s, password: %(password)s, "
"tenant_name: %(tenant_name)s") % locals()
raise exceptions.InvalidConfiguration(msg)
- auth_url = self.config.identity.auth_url
+ self.auth_url = self.config.identity.auth_url
if self.config.identity.strategy == 'keystone':
- client_args = (self.config, username, password, auth_url,
- tenant_name)
+ client_args = (self.config, self.username, self.password,
+ self.auth_url, self.tenant_name)
else:
- client_args = (self.config, username, password, auth_url)
+ client_args = (self.config, self.username, self.password,
+ self.auth_url)
try:
self.servers_client = SERVERS_CLIENTS[interface](*client_args)
@@ -119,9 +122,10 @@
self.images_client = ImagesClient(*client_args)
self.security_groups_client = SecurityGroupsClient(*client_args)
self.floating_ips_client = FloatingIPsClient(*client_args)
- self.volumes_client = VolumesClient(*client_args)
self.console_outputs_client = ConsoleOutputsClient(*client_args)
self.network_client = NetworkClient(*client_args)
+ self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
+ self.volumes_client = VolumesClient(*client_args)
class AltManager(Manager):
diff --git a/tempest/services/identity/json/admin_client.py b/tempest/services/identity/json/admin_client.py
index 89a068e..cb9c10b 100644
--- a/tempest/services/identity/json/admin_client.py
+++ b/tempest/services/identity/json/admin_client.py
@@ -101,6 +101,13 @@
body = json.loads(body)
return resp, body['tenants']
+ def get_tenant_by_name(self, tenant_name):
+ resp, tenants = self.list_tenants()
+ for tenant in tenants:
+ if tenant['name'] == tenant_name:
+ return tenant
+ raise exceptions.NotFound('No such tenant')
+
def update_tenant(self, tenant_id, **kwargs):
"""Updates a tenant"""
resp, body = self.get_tenant(tenant_id)
@@ -165,6 +172,13 @@
body = json.loads(body)
return resp, body['users']
+ def get_user_by_username(self, tenant_id, username):
+ resp, users = self.list_users_for_tenant(tenant_id)
+ for user in users:
+ if user['name'] == username:
+ return user
+ raise exceptions.NotFound('No such user')
+
def create_service(self, name, type, **kwargs):
"""Create a service"""
post_body = {
diff --git a/tempest/services/image/service.py b/tempest/services/image/service.py
index 9cfe502..154b5b8 100644
--- a/tempest/services/image/service.py
+++ b/tempest/services/image/service.py
@@ -36,22 +36,25 @@
# Determine the Images API version
self.api_version = int(config.images.api_version)
+ # We load the client class specific to the API version...
if self.api_version == 1:
- # We load the client class specific to the API version...
- from glance import client
- creds = {
- 'username': config.images.username,
- 'password': config.images.password,
- 'tenant': config.images.tenant_name,
- # rstrip() is necessary here because Glance client
- # automatically adds the tokens/ part...
- 'auth_url': config.identity.auth_url.rstrip('/tokens'),
- 'strategy': config.identity.strategy
- }
- self._client = client.Client(config.images.host,
- config.images.port,
- creds=creds,
- configure_via_auth=False)
+ import glanceclient
+ import keystoneclient.v2_0.client
+
+ auth_url = self.config.identity.auth_url.rstrip('tokens')
+ keystone = keystoneclient.v2_0.client.Client(
+ username=config.images.username,
+ password=config.images.password,
+ tenant_name=config.images.tenant_name,
+ auth_url=auth_url)
+ token = keystone.auth_token
+ endpoint = keystone.service_catalog.url_for(
+ service_type='image',
+ endpoint_type='publicURL')
+
+ self._client = glanceclient.Client('1',
+ endpoint=endpoint,
+ token=token)
else:
raise NotImplementedError
diff --git a/tempest/services/nova/json/volumes_client.py b/tempest/services/nova/json/volumes_extensions_client.py
similarity index 89%
rename from tempest/services/nova/json/volumes_client.py
rename to tempest/services/nova/json/volumes_extensions_client.py
index fd577b7..9b3590b 100644
--- a/tempest/services/nova/json/volumes_client.py
+++ b/tempest/services/nova/json/volumes_extensions_client.py
@@ -4,14 +4,15 @@
import time
-class VolumesClient(RestClient):
+class VolumesExtensionsClient(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(VolumesClient, self).__init__(config, username, password,
- auth_url, tenant_name)
+ super(VolumesExtensionsClient, self).__init__(config, username,
+ password, auth_url,
+ tenant_name)
self.service = self.config.compute.catalog_type
- self.build_interval = self.config.compute.build_interval
- self.build_timeout = self.config.compute.build_timeout
+ self.build_interval = self.config.volume.build_interval
+ self.build_timeout = self.config.volume.build_timeout
def list_volumes(self, params=None):
"""List all the volumes created"""
diff --git a/tempest/services/volume/__init__.py b/tempest/services/volume/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/__init__.py
diff --git a/tempest/services/volume/json/__init__.py b/tempest/services/volume/json/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/json/__init__.py
diff --git a/tempest/services/nova/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
similarity index 70%
copy from tempest/services/nova/json/volumes_client.py
copy to tempest/services/volume/json/volumes_client.py
index fd577b7..68c7a86 100644
--- a/tempest/services/nova/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -1,35 +1,56 @@
-from tempest import exceptions
-from tempest.common.rest_client import RestClient
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 json
import time
+from tempest.common.rest_client import RestClient
+from tempest import exceptions
+
class VolumesClient(RestClient):
+ """
+ Client class to send CRUD Volume API requests to a Cinder endpoint
+ """
def __init__(self, config, username, password, auth_url, tenant_name=None):
super(VolumesClient, self).__init__(config, username, password,
auth_url, tenant_name)
- self.service = self.config.compute.catalog_type
- self.build_interval = self.config.compute.build_interval
- self.build_timeout = self.config.compute.build_timeout
+
+ self.service = self.config.volume.catalog_type
+ self.build_interval = self.config.volume.build_interval
+ self.build_timeout = self.config.volume.build_timeout
def list_volumes(self, params=None):
"""List all the volumes created"""
- url = 'os-volumes'
+ url = 'volumes'
if params != None:
param_list = []
for param, value in params.iteritems():
param_list.append("%s=%s&" % (param, value))
url += '?' + ' '.join(param_list)
-
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volumes']
def list_volumes_with_detail(self, params=None):
- """List all the details of volumes"""
- url = 'os-volumes/detail'
+ """List the details of all volumes"""
+ url = 'volumes/detail'
if params != None:
param_list = []
for param, value in params.iteritems():
@@ -43,7 +64,7 @@
def get_volume(self, volume_id):
"""Returns the details of a single volume"""
- url = "os-volumes/%s" % str(volume_id)
+ url = "volumes/%s" % str(volume_id)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['volume']
@@ -63,18 +84,18 @@
}
post_body = json.dumps({'volume': post_body})
- resp, body = self.post('os-volumes', post_body, self.headers)
+ resp, body = self.post('volumes', post_body, self.headers)
body = json.loads(body)
return resp, body['volume']
def delete_volume(self, volume_id):
"""Deletes the Specified Volume"""
- return self.delete("os-volumes/%s" % str(volume_id))
+ return self.delete("volumes/%s" % str(volume_id))
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status"""
resp, body = self.get_volume(volume_id)
- volume_name = body['displayName']
+ volume_name = body['display_name']
volume_status = body['status']
start = int(time.time())
diff --git a/tempest/smoke.py b/tempest/smoke.py
index 9383559..c929273 100644
--- a/tempest/smoke.py
+++ b/tempest/smoke.py
@@ -38,10 +38,11 @@
pass
-class ComputeSmokeTest(test.ComputeDefaultClientTest, SmokeTest):
+class DefaultClientSmokeTest(test.DefaultClientTest, SmokeTest):
"""
- Base smoke test case class for OpenStack Compute API (Nova)
+ Base smoke test case class that provides the default clients to
+ access the various OpenStack APIs.
"""
@classmethod
diff --git a/tempest/test.py b/tempest/test.py
index 33bb3fb..4bb8ca4 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -38,7 +38,12 @@
def setUpClass(cls):
cls.manager = cls.manager_class()
cls.config = cls.manager.config
- cls.client = cls.manager.client
+ for attr_name in cls.manager.client_attr_names:
+ # Ensure that pre-existing class attributes won't be
+ # accidentally overriden.
+ assert not hasattr(cls, attr_name)
+ client = getattr(cls.manager, attr_name)
+ setattr(cls, attr_name, client)
cls.resource_keys = {}
cls.resources = []
@@ -57,14 +62,14 @@
del self.resource_keys[key]
-class ComputeDefaultClientTest(TestCase):
+class DefaultClientTest(TestCase):
"""
- Base test case class for OpenStack Compute API (Nova)
- that uses the novaclient libs for calling the API.
+ Base test case class that provides the default clients to access
+ the various OpenStack APIs.
"""
- manager_class = manager.ComputeDefaultClientManager
+ manager_class = manager.DefaultClientManager
def status_timeout(self, things, thing_id, expected_status):
"""
diff --git a/tempest/tests/compute/admin/test_flavors.py b/tempest/tests/compute/admin/test_flavors.py
index be77411..9c38ff4 100644
--- a/tempest/tests/compute/admin/test_flavors.py
+++ b/tempest/tests/compute/admin/test_flavors.py
@@ -46,7 +46,6 @@
cls.rxtx = 1
@attr(type='positive')
- @unittest.skip("Until Bug 1042539 is fixed")
def test_create_flavor(self):
"""Create a flavor and ensure it is listed
This operation requires the user to have 'admin' role"""
@@ -77,7 +76,6 @@
self.assertEqual(resp.status, 202)
@attr(type='positive')
- @unittest.skip("Until Bug 1042539 is fixed")
def test_create_flavor_verify_entry_in_list_details(self):
"""Create a flavor and ensure it's details are listed
This operation requires the user to have 'admin' role"""
@@ -102,7 +100,6 @@
self.assertEqual(resp.status, 202)
@attr(type='negative')
- @unittest.skip("Until Bug 1042539 is fixed")
def test_get_flavor_details_for_deleted_flavor(self):
"""Delete a flavor and ensure it is not listed"""
# Create a test flavor
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index ba4f518..2617fbb 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -17,11 +17,14 @@
import logging
import time
+import nose
import unittest2 as unittest
+import nose
from tempest import config
from tempest import openstack
+from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
from tempest.services.identity.json.admin_client import AdminClient
@@ -60,6 +63,7 @@
cls.security_groups_client = os.security_groups_client
cls.console_outputs_client = os.console_outputs_client
cls.limits_client = os.limits_client
+ cls.volumes_extensions_client = os.volumes_extensions_client
cls.volumes_client = os.volumes_client
cls.build_interval = cls.config.compute.build_interval
cls.build_timeout = cls.config.compute.build_timeout
@@ -101,12 +105,37 @@
tenant_desc = tenant_name + "-desc"
password = "pass"
- resp, tenant = admin_client.create_tenant(name=tenant_name,
- description=tenant_desc)
- resp, user = admin_client.create_user(username,
- password,
- tenant['id'],
- email)
+ try:
+ resp, tenant = admin_client.create_tenant(name=tenant_name,
+ description=tenant_desc)
+ except exceptions.Duplicate:
+ if cls.config.compute.allow_tenant_reuse:
+ tenant = admin_client.get_tenant_by_name(tenant_name)
+ LOG.info('Re-using existing tenant %s' % tenant)
+ else:
+ msg = ('Unable to create isolated tenant %s because ' +
+ 'it already exists. If this is related to a ' +
+ 'previous test failure, try using ' +
+ 'allow_tentant_reuse in tempest.conf') % tenant_name
+ raise exceptions.Duplicate(msg)
+
+ try:
+ resp, user = admin_client.create_user(username,
+ password,
+ tenant['id'],
+ email)
+ except exceptions.Duplicate:
+ if cls.config.compute.allow_tenant_reuse:
+ user = admin_client.get_user_by_username(tenant['id'],
+ username)
+ LOG.info('Re-using existing user %s' % user)
+ else:
+ msg = ('Unable to create isolated tenant %s because ' +
+ 'it already exists. If this is related to a ' +
+ 'previous test failure, try using ' +
+ 'allow_tentant_reuse in tempest.conf') % tenant_name
+ raise exceptions.Duplicate(msg)
+
# Store the complete creds (including UUID ids...) for later
# but return just the username, tenant_name, password tuple
# that the various clients will use.
diff --git a/tempest/tests/compute/test_authorization.py b/tempest/tests/compute/test_authorization.py
index 0f0a8dd..afa60b3 100644
--- a/tempest/tests/compute/test_authorization.py
+++ b/tempest/tests/compute/test_authorization.py
@@ -174,7 +174,6 @@
"""A create image request for another user's server should fail"""
self.alt_images_client.create_image(self.server['id'], 'testImage')
- @unittest.skip("Skipped until the Bug #1039608 is resolved")
@raises(exceptions.BadRequest)
@attr(type='negative')
def test_create_server_with_unauthorized_image(self):
diff --git a/tempest/tests/compute/test_images.py b/tempest/tests/compute/test_images.py
index c969a0c..694965f 100644
--- a/tempest/tests/compute/test_images.py
+++ b/tempest/tests/compute/test_images.py
@@ -17,6 +17,7 @@
from nose.plugins.attrib import attr
import unittest2 as unittest
+import nose
from tempest.common.utils.data_utils import rand_name, parse_image_id
import tempest.config
@@ -381,6 +382,7 @@
@classmethod
def setUpClass(cls):
+ raise nose.SkipTest("Until Bug 1046870 is fixed")
super(ImagesTestJSON, cls).setUpClass()
cls.client = cls.images_client
cls.servers_client = cls.servers_client
@@ -407,6 +409,7 @@
@classmethod
def setUpClass(cls):
+ raise nose.SkipTest("Until Bug 1046870 is fixed")
super(ImagesTestXML, cls).setUpClass()
cls.client = cls.images_client
cls.servers_client = cls.servers_client
diff --git a/tempest/tests/compute/test_server_advanced_ops.py b/tempest/tests/compute/test_server_advanced_ops.py
index d6962f4..4e85b04 100644
--- a/tempest/tests/compute/test_server_advanced_ops.py
+++ b/tempest/tests/compute/test_server_advanced_ops.py
@@ -25,7 +25,7 @@
LOG = logging.getLogger(__name__)
-class TestServerAdvancedOps(test.ComputeDefaultClientTest):
+class TestServerAdvancedOps(test.DefaultClientTest):
"""
This test case stresses some advanced server instance operations:
@@ -57,7 +57,7 @@
i_name = rand_name('instance')
flavor_id = self.config.compute.flavor_ref
base_image_id = self.config.compute.image_ref
- self.instance = self.client.servers.create(
+ self.instance = self.compute_client.servers.create(
i_name, base_image_id, flavor_id)
try:
self.assertEqual(self.instance.name, i_name)
@@ -67,16 +67,16 @@
self.assertEqual(self.instance.status, 'BUILD')
instance_id = self.get_resource('instance').id
- self.status_timeout(self.client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
instance = self.get_resource('instance')
instance_id = instance.id
resize_flavor = self.config.compute.flavor_ref_alt
-
LOG.debug("Resizing instance %s from flavor %s to flavor %s",
instance.id, instance.flavor, resize_flavor)
instance.resize(resize_flavor)
- self.status_timeout(self.client.servers, instance_id, 'VERIFY_RESIZE')
+ self.status_timeout(self.compute_client.servers, instance_id,
+ 'VERIFY_RESIZE')
LOG.debug("Confirming resize of instance %s", instance_id)
instance.confirm_resize()
- self.status_timeout(self.client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
diff --git a/tempest/tests/compute/test_server_basic_ops.py b/tempest/tests/compute/test_server_basic_ops.py
index 8af5212..04b1451 100644
--- a/tempest/tests/compute/test_server_basic_ops.py
+++ b/tempest/tests/compute/test_server_basic_ops.py
@@ -23,7 +23,7 @@
LOG = logging.getLogger(__name__)
-class TestServerBasicOps(smoke.ComputeSmokeTest):
+class TestServerBasicOps(smoke.DefaultClientSmokeTest):
"""
This smoke test case follows this basic set of operations:
@@ -39,7 +39,7 @@
def test_001_create_keypair(self):
kp_name = rand_name('keypair-smoke')
- self.keypair = self.client.keypairs.create(kp_name)
+ self.keypair = self.compute_client.keypairs.create(kp_name)
try:
self.assertEqual(self.keypair.id, kp_name)
self.set_resource('keypair', self.keypair)
@@ -49,7 +49,8 @@
def test_002_create_security_group(self):
sg_name = rand_name('secgroup-smoke')
sg_desc = sg_name + " description"
- self.secgroup = self.client.security_groups.create(sg_name, sg_desc)
+ self.secgroup = self.compute_client.security_groups.create(sg_name,
+ sg_desc)
try:
self.assertEqual(self.secgroup.name, sg_name)
self.assertEqual(self.secgroup.description, sg_desc)
@@ -76,8 +77,8 @@
]
for ruleset in rulesets:
try:
- self.client.security_group_rules.create(
- self.secgroup.id, **ruleset)
+ self.compute_client.security_group_rules.create(
+ self.secgroup.id, **ruleset)
except:
self.fail("Failed to create rule in security group.")
@@ -88,7 +89,7 @@
create_kwargs = {
'key_name': self.get_resource('keypair').id
}
- self.instance = self.client.servers.create(
+ self.instance = self.compute_client.servers.create(
i_name, base_image_id, flavor_id, **create_kwargs)
try:
self.assertEqual(self.instance.name, i_name)
@@ -100,7 +101,7 @@
def test_004_wait_on_active(self):
instance_id = self.get_resource('instance').id
- self.status_timeout(self.client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
def test_005_pause_server(self):
instance = self.get_resource('instance')
@@ -108,7 +109,7 @@
LOG.debug("Pausing instance %s. Current status: %s",
instance_id, instance.status)
instance.pause()
- self.status_timeout(self.client.servers, instance_id, 'PAUSED')
+ self.status_timeout(self.compute_client.servers, instance_id, 'PAUSED')
def test_006_unpause_server(self):
instance = self.get_resource('instance')
@@ -116,7 +117,7 @@
LOG.debug("Unpausing instance %s. Current status: %s",
instance_id, instance.status)
instance.unpause()
- self.status_timeout(self.client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
def test_007_suspend_server(self):
instance = self.get_resource('instance')
@@ -124,7 +125,8 @@
LOG.debug("Suspending instance %s. Current status: %s",
instance_id, instance.status)
instance.suspend()
- self.status_timeout(self.client.servers, instance_id, 'SUSPENDED')
+ self.status_timeout(self.compute_client.servers,
+ instance_id, 'SUSPENDED')
def test_008_resume_server(self):
instance = self.get_resource('instance')
@@ -132,7 +134,7 @@
LOG.debug("Resuming instance %s. Current status: %s",
instance_id, instance.status)
instance.resume()
- self.status_timeout(self.client.servers, instance_id, 'ACTIVE')
+ self.status_timeout(self.compute_client.servers, instance_id, 'ACTIVE')
def test_099_terminate_instance(self):
instance = self.get_resource('instance')
diff --git a/tempest/tests/compute/test_servers_negative.py b/tempest/tests/compute/test_servers_negative.py
index 5d7742b..2558e6d 100644
--- a/tempest/tests/compute/test_servers_negative.py
+++ b/tempest/tests/compute/test_servers_negative.py
@@ -19,6 +19,7 @@
from nose.plugins.attrib import attr
import unittest2 as unittest
+import nose
from tempest import exceptions
from tempest import openstack
@@ -30,6 +31,7 @@
@classmethod
def setUpClass(cls):
+ raise nose.SkipTest("Until Bug 1046870 is fixed")
super(ServersNegativeTest, cls).setUpClass()
cls.client = cls.servers_client
cls.img_client = cls.images_client
diff --git a/tempest/tests/compute/test_volumes_get.py b/tempest/tests/compute/test_volumes_get.py
index a7f21cb..cda943d 100644
--- a/tempest/tests/compute/test_volumes_get.py
+++ b/tempest/tests/compute/test_volumes_get.py
@@ -16,7 +16,6 @@
# under the License.
from nose.plugins.attrib import attr
-import unittest2 as unittest
from tempest.common.utils.data_utils import rand_name
from tempest.tests.compute.base import BaseComputeTest
@@ -27,7 +26,7 @@
@classmethod
def setUpClass(cls):
super(VolumesGetTest, cls).setUpClass()
- cls.client = cls.volumes_client
+ cls.client = cls.volumes_extensions_client
@attr(type='smoke')
def test_volume_create_get_delete(self):
@@ -64,6 +63,7 @@
fetched_volume['metadata'],
'The fetched Volume is different '
'from the created Volume')
+
finally:
#Delete the Volume created in this method
resp, _ = self.client.delete_volume(volume['id'])
diff --git a/tempest/tests/compute/test_volumes_list.py b/tempest/tests/compute/test_volumes_list.py
index c3bde15..679a23b 100644
--- a/tempest/tests/compute/test_volumes_list.py
+++ b/tempest/tests/compute/test_volumes_list.py
@@ -15,30 +15,26 @@
# License for the specific language governing permissions and limitations
# under the License.
-import unittest2 as unittest
-
import nose
from tempest.common.utils.data_utils import rand_name
from tempest.tests.compute.base import BaseComputeTest
-class VolumesTest(BaseComputeTest):
+class VolumesListTest(BaseComputeTest):
"""
This test creates a number of 1G volumes. To run successfully,
ensure that the backing file for the volume group that Nova uses
- has space for at least 3 1G volumes! Devstack, by default, creates
- a 2G volume backing file, which causes this test to fail because
- the third volume gets created in ERROR state (out of disk space in
- volume group...). If you are running a Devstack environment, set
- VOLUME_BACKING_FILE_SIZE=4G in your localrc
+ has space for at least 3 1G volumes!
+ If you are running a Devstack environment, ensure that the
+ VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
"""
@classmethod
def setUpClass(cls):
- super(VolumesTest, cls).setUpClass()
- cls.client = cls.volumes_client
+ super(VolumesListTest, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
# Create 3 Volumes
cls.volume_list = list()
cls.volume_id_list = list()
@@ -75,7 +71,7 @@
# Delete the created Volumes
for volume in cls.volume_list:
resp, _ = cls.client.delete_volume(volume['id'])
- super(VolumesTest, cls).tearDownClass()
+ super(VolumesListTest, cls).tearDownClass()
def test_volume_list(self):
"""Should return the list of Volumes"""
diff --git a/tempest/tests/compute/test_volumes_negative.py b/tempest/tests/compute/test_volumes_negative.py
index 8d67fff..ea2811c 100644
--- a/tempest/tests/compute/test_volumes_negative.py
+++ b/tempest/tests/compute/test_volumes_negative.py
@@ -17,7 +17,6 @@
from nose.plugins.attrib import attr
from nose.tools import raises
-import unittest2 as unittest
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
@@ -29,7 +28,7 @@
@classmethod
def setUpClass(cls):
super(VolumesNegativeTest, cls).setUpClass()
- cls.client = cls.volumes_client
+ cls.client = cls.volumes_extensions_client
@attr(type='negative')
def test_volume_get_nonexistant_volume_id(self):
diff --git a/tempest/tests/identity/admin/test_roles.py b/tempest/tests/identity/admin/test_roles.py
index 4256da5..813f64a 100644
--- a/tempest/tests/identity/admin/test_roles.py
+++ b/tempest/tests/identity/admin/test_roles.py
@@ -237,10 +237,12 @@
(user, tenant, role) = self._get_role_params()
token = self.client.get_auth()
self.client.delete_token(token)
- self.assertRaises(exceptions.Unauthorized,
- self.client.list_user_roles, tenant['id'],
- user['id'])
- self.client.clear_auth()
+ try:
+ self.assertRaises(exceptions.Unauthorized,
+ self.client.list_user_roles, tenant['id'],
+ user['id'])
+ finally:
+ self.client.clear_auth()
def test_list_user_roles_for_non_existent_user(self):
"""Attempt to list roles of a non existent user should fail"""
diff --git a/tempest/tests/image/test_images.py b/tempest/tests/image/test_images.py
index 0e515dc..c6b903b 100644
--- a/tempest/tests/image/test_images.py
+++ b/tempest/tests/image/test_images.py
@@ -25,8 +25,7 @@
GLANCE_INSTALLED = False
try:
- from glance import client
- from glance.common import exception
+ import glanceclient
GLANCE_INSTALLED = True
except ImportError:
pass
@@ -51,7 +50,7 @@
@classmethod
def tearDownClass(cls):
for image_id in cls.created_images:
- cls.client.delete_image(image_id)
+ cls.client.images.delete(image_id)
@attr(type='image')
def test_register_with_invalid_data(self):
@@ -70,11 +69,10 @@
]
for meta in metas:
try:
- results = self.client.add_image(meta)
- except exception.Invalid:
+ self.client.images.create(**meta)
+ except glanceclient.exc.HTTPBadRequest:
continue
- self.fail("Did not raise Invalid for meta %s. Got results: %s" %
- (meta, results))
+ self.fail("Did not raise Invalid for meta %s." % meta)
@attr(type='image')
def test_register_then_upload(self):
@@ -86,27 +84,27 @@
'container_format': 'bare',
'properties': {'prop1': 'val1'}
}
- results = self.client.add_image(meta)
- self.assertTrue('id' in results)
- image_id = results['id']
+ results = self.client.images.create(**meta)
+ self.assertTrue(hasattr(results, 'id'))
+ image_id = results.id
self.created_images.append(image_id)
- self.assertTrue('name' in results)
- self.assertEqual(meta['name'], results['name'])
- self.assertTrue('is_public' in results)
- self.assertEqual(meta['is_public'], results['is_public'])
- self.assertTrue('status' in results)
- self.assertEqual('queued', results['status'])
- self.assertTrue('properties' in results)
+ self.assertTrue(hasattr(results, 'name'))
+ self.assertEqual(meta['name'], results.name)
+ self.assertTrue(hasattr(results, 'is_public'))
+ self.assertEqual(meta['is_public'], results.is_public)
+ self.assertTrue(hasattr(results, 'status'))
+ self.assertEqual('queued', results.status)
+ self.assertTrue(hasattr(results, 'properties'))
for key, val in meta['properties'].items():
- self.assertEqual(val, results['properties'][key])
+ self.assertEqual(val, results.properties[key])
# Now try uploading an image file
image_file = StringIO.StringIO('*' * 1024)
- results = self.client.update_image(image_id, image_data=image_file)
- self.assertTrue('status' in results)
- self.assertEqual('active', results['status'])
- self.assertTrue('size' in results)
- self.assertEqual(1024, results['size'])
+ results = self.client.images.update(image_id, data=image_file)
+ self.assertTrue(hasattr(results, 'status'))
+ self.assertEqual('active', results.status)
+ self.assertTrue(hasattr(results, 'size'))
+ self.assertEqual(1024, results.size)
@attr(type='image')
def test_register_remote_image(self):
@@ -118,16 +116,16 @@
'container_format': 'bare',
'location': 'http://example.com/someimage.iso'
}
- results = self.client.add_image(meta)
- self.assertTrue('id' in results)
- image_id = results['id']
+ results = self.client.images.create(**meta)
+ self.assertTrue(hasattr(results, 'id'))
+ image_id = results.id
self.created_images.append(image_id)
- self.assertTrue('name' in results)
- self.assertEqual(meta['name'], results['name'])
- self.assertTrue('is_public' in results)
- self.assertEqual(meta['is_public'], results['is_public'])
- self.assertTrue('status' in results)
- self.assertEqual('active', results['status'])
+ self.assertTrue(hasattr(results, 'name'))
+ self.assertEqual(meta['name'], results.name)
+ self.assertTrue(hasattr(results, 'is_public'))
+ self.assertEqual(meta['is_public'], results.is_public)
+ self.assertTrue(hasattr(results, 'status'))
+ self.assertEqual('active', results.status)
class ListImagesTest(unittest.TestCase):
@@ -143,7 +141,7 @@
cls.os = openstack.ServiceManager()
cls.client = cls.os.images.get_client()
cls.created_images = []
- cls.original_images = cls.client.get_images()
+ cls.original_images = list(cls.client.images.list())
# We add a few images here to test the listing functionality of
# the images API
@@ -157,7 +155,7 @@
@classmethod
def tearDownClass(cls):
for image_id in cls.created_images:
- cls.client.delete_image(image_id)
+ cls.client.images.delete(image_id)
@classmethod
def _create_remote_image(cls, x):
@@ -172,8 +170,8 @@
'container_format': 'bare',
'location': 'http://example.com/someimage_%s.iso' % x
}
- results = cls.client.add_image(meta)
- image_id = results['id']
+ results = cls.client.images.create(**meta)
+ image_id = results.id
return image_id
@classmethod
@@ -183,15 +181,16 @@
image. Note that the size of the new image is a random number between
1024 and 4096
"""
+ image_file = StringIO.StringIO('*' * random.randint(1024, 4096))
meta = {
'name': 'New Standard Image %s' % x,
'is_public': True,
'disk_format': 'raw',
- 'container_format': 'bare'
+ 'container_format': 'bare',
+ 'data': image_file,
}
- image_file = StringIO.StringIO('*' * random.randint(1024, 4096))
- results = cls.client.add_image(meta, image_file)
- image_id = results['id']
+ results = cls.client.images.create(**meta)
+ image_id = results.id
return image_id
@attr(type='image')
@@ -199,5 +198,5 @@
"""
Simple test to see all fixture images returned
"""
- images = self.client.get_images()
+ images = list(self.client.images.list())
self.assertEqual(10, len(images) - len(self.original_images))
diff --git a/tempest/tests/volume/__init__.py b/tempest/tests/volume/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/volume/__init__.py
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
new file mode 100644
index 0000000..41f08fe
--- /dev/null
+++ b/tempest/tests/volume/base.py
@@ -0,0 +1,149 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 logging
+import time
+import nose
+
+import unittest2 as unittest
+
+from tempest import config
+from tempest import openstack
+from tempest.common.utils.data_utils import rand_name
+from tempest.services.identity.json.admin_client import AdminClient
+from tempest import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+class BaseVolumeTest(unittest.TestCase):
+
+ """Base test case class for all Cinder API tests"""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.config = config.TempestConfig()
+ cls.isolated_creds = []
+
+ if cls.config.compute.allow_tenant_isolation:
+ creds = cls._get_isolated_creds()
+ username, tenant_name, password = creds
+ os = openstack.Manager(username=username,
+ password=password,
+ tenant_name=tenant_name)
+ else:
+ os = openstack.Manager()
+
+ cls.os = os
+ cls.volumes_client = os.volumes_client
+ cls.build_interval = cls.config.volume.build_interval
+ cls.build_timeout = cls.config.volume.build_timeout
+ cls.volumes = {}
+
+ skip_msg = ("%s skipped as Cinder endpoint is not available" %
+ cls.__name__)
+ try:
+ cls.volumes_client.keystone_auth(cls.os.username,
+ cls.os.password,
+ cls.os.auth_url,
+ cls.volumes_client.service,
+ cls.os.tenant_name)
+ except exceptions.EndpointNotFound:
+ raise nose.SkipTest(skip_msg)
+
+ @classmethod
+ def _get_identity_admin_client(cls):
+ """
+ Returns an instance of the Identity Admin API client
+ """
+ client_args = (cls.config,
+ cls.config.identity_admin.username,
+ cls.config.identity_admin.password,
+ cls.config.identity.auth_url)
+ tenant_name = cls.config.identity_admin.tenant_name
+ admin_client = AdminClient(*client_args, tenant_name=tenant_name)
+ return admin_client
+
+ @classmethod
+ def _get_isolated_creds(cls):
+ """
+ Creates a new set of user/tenant/password credentials for a
+ **regular** user of the Volume API so that a test case can
+ operate in an isolated tenant container.
+ """
+ admin_client = cls._get_identity_admin_client()
+ rand_name_root = cls.__name__
+ if cls.isolated_creds:
+ # Main user already created. Create the alt one...
+ rand_name_root += '-alt'
+ username = rand_name_root + "-user"
+ email = rand_name_root + "@example.com"
+ tenant_name = rand_name_root + "-tenant"
+ tenant_desc = tenant_name + "-desc"
+ password = "pass"
+
+ resp, tenant = admin_client.create_tenant(name=tenant_name,
+ description=tenant_desc)
+ resp, user = admin_client.create_user(username,
+ password,
+ tenant['id'],
+ email)
+ # Store the complete creds (including UUID ids...) for later
+ # but return just the username, tenant_name, password tuple
+ # that the various clients will use.
+ cls.isolated_creds.append((user, tenant))
+
+ return username, tenant_name, password
+
+ @classmethod
+ def clear_isolated_creds(cls):
+ if not cls.isolated_creds:
+ pass
+ admin_client = cls._get_identity_admin_client()
+
+ for user, tenant in cls.isolated_creds:
+ admin_client.delete_user(user['id'])
+ admin_client.delete_tenant(tenant['id'])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.clear_isolated_creds()
+
+ def create_volume(self, size=1, metadata={}):
+ """Wrapper utility that returns a test volume"""
+ display_name = rand_name(self.__class__.__name__ + "-volume")
+ resp, volume = self.volumes_client.create_volume(size=size,
+ display_name=display_name,
+ metdata=metadata)
+ self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ self.volumes.append(volume)
+ return volume
+
+ def wait_for(self, condition):
+ """Repeatedly calls condition() until a timeout"""
+ start_time = int(time.time())
+ while True:
+ try:
+ condition()
+ except:
+ pass
+ else:
+ return
+ if int(time.time()) - start_time >= self.build_timeout:
+ condition()
+ return
+ time.sleep(self.build_interval)
diff --git a/tempest/tests/volume/test_volumes_get.py b/tempest/tests/volume/test_volumes_get.py
new file mode 100644
index 0000000..4305c67
--- /dev/null
+++ b/tempest/tests/volume/test_volumes_get.py
@@ -0,0 +1,99 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 nose.plugins.attrib import attr
+
+from tempest.common.utils.data_utils import rand_name
+from tempest.tests.volume.base import BaseVolumeTest
+
+
+class VolumesGetTest(BaseVolumeTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesGetTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ @attr(type='smoke')
+ def test_volume_create_get_delete(self):
+ """Create a volume, Get it's details and Delete the volume"""
+ try:
+ volume = {}
+ v_name = rand_name('Volume-')
+ metadata = {'Type': 'work'}
+ #Create a volume
+ resp, volume = self.client.create_volume(size=1,
+ display_name=v_name,
+ metadata=metadata)
+ self.assertEqual(200, resp.status)
+ self.assertTrue('id' in volume)
+ self.assertTrue('display_name' in volume)
+ self.assertEqual(volume['display_name'], v_name,
+ "The created volume name is not equal to the requested name")
+ self.assertTrue(volume['id'] is not None,
+ "Field volume id is empty or not found.")
+ self.client.wait_for_volume_status(volume['id'], 'available')
+ # Get Volume information
+ resp, fetched_volume = self.client.get_volume(volume['id'])
+ self.assertEqual(200, resp.status)
+ self.assertEqual(v_name,
+ fetched_volume['display_name'],
+ 'The fetched Volume is different '
+ 'from the created Volume')
+ self.assertEqual(volume['id'],
+ fetched_volume['id'],
+ 'The fetched Volume is different '
+ 'from the created Volume')
+ self.assertEqual(metadata,
+ fetched_volume['metadata'],
+ 'The fetched Volume is different '
+ 'from the created Volume')
+ except:
+ self.fail("Could not create a volume")
+ finally:
+ if volume:
+ # Delete the Volume if it was created
+ resp, _ = self.client.delete_volume(volume['id'])
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_resource_deletion(volume['id'])
+
+ @attr(type='positive')
+ def test_volume_get_metadata_none(self):
+ """Create a volume without passing metadata, get details, and delete"""
+ try:
+ volume = {}
+ v_name = rand_name('Volume-')
+ # Create a volume without metadata
+ resp, volume = self.client.create_volume(size=1,
+ display_name=v_name,
+ metadata={})
+ self.assertEqual(200, resp.status)
+ self.assertTrue('id' in volume)
+ self.assertTrue('display_name' in volume)
+ self.client.wait_for_volume_status(volume['id'], 'available')
+ #GET Volume
+ resp, fetched_volume = self.client.get_volume(volume['id'])
+ self.assertEqual(200, resp.status)
+ self.assertEqual(fetched_volume['metadata'], {})
+ except:
+ self.fail("Could not get volume metadata")
+ finally:
+ if volume:
+ # Delete the Volume if it was created
+ resp, _ = self.client.delete_volume(volume['id'])
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_resource_deletion(volume['id'])
diff --git a/tempest/tests/volume/test_volumes_list.py b/tempest/tests/volume/test_volumes_list.py
new file mode 100644
index 0000000..24055af
--- /dev/null
+++ b/tempest/tests/volume/test_volumes_list.py
@@ -0,0 +1,103 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 nose
+from nose.plugins.attrib import attr
+
+from tempest.common.utils.data_utils import rand_name
+from tempest.tests.volume.base import BaseVolumeTest
+
+
+class VolumesListTest(BaseVolumeTest):
+
+ """
+ This test creates a number of 1G volumes. To run successfully,
+ ensure that the backing file for the volume group that Nova uses
+ has space for at least 3 1G volumes!
+ If you are running a Devstack environment, ensure that the
+ VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesListTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ # Create 3 test volumes
+ cls.volume_list = []
+ cls.volume_id_list = []
+ for i in range(3):
+ v_name = rand_name('volume')
+ metadata = {'Type': 'work'}
+ try:
+ resp, volume = cls.client.create_volume(size=1,
+ display_name=v_name,
+ metadata=metadata)
+ cls.client.wait_for_volume_status(volume['id'],
+ 'available')
+ resp, volume = cls.client.get_volume(volume['id'])
+ cls.volume_list.append(volume)
+ cls.volume_id_list.append(volume['id'])
+ except:
+ if cls.volume_list:
+ # We could not create all the volumes, though we were able
+ # to create *some* of the volumes. This is typically
+ # because the backing file size of the volume group is
+ # too small. So, here, we clean up whatever we did manage
+ # to create and raise a SkipTest
+ for volume in cls.volume_id_list:
+ cls.client.delete_volume(volume)
+ msg = ("Failed to create ALL necessary volumes to run "
+ "test. This typically means that the backing file "
+ "size of the nova-volumes group is too small to "
+ "create the 3 volumes needed by this test case")
+ raise nose.SkipTest(msg)
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ # Delete the created volumes
+ for volume in cls.volume_id_list:
+ resp, _ = cls.client.delete_volume(volume)
+ cls.client.wait_for_resource_deletion(volume)
+ super(VolumesListTest, cls).tearDownClass()
+
+ @attr(type='smoke')
+ def test_volume_list(self):
+ """Get a list of Volumes"""
+ # Fetch all volumes
+ resp, fetched_list = self.client.list_volumes()
+ self.assertEqual(200, resp.status)
+ # Now check if all the volumes created in setup are in fetched list
+ missing_vols = [v for v in self.volume_list if v not in fetched_list]
+ self.assertFalse(missing_vols,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['display_name']
+ for m_vol in missing_vols))
+
+ @attr(type='smoke')
+ def test_volume_list_with_details(self):
+ """Get a list of Volumes with details"""
+ # Fetch all Volumes
+ resp, fetched_list = self.client.list_volumes_with_detail()
+ self.assertEqual(200, resp.status)
+ # Verify that all the volumes are returned
+ missing_vols = [v for v in self.volume_list if v not in fetched_list]
+ self.assertFalse(missing_vols,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['display_name']
+ for m_vol in missing_vols))
diff --git a/tempest/tests/volume/test_volumes_negative.py b/tempest/tests/volume/test_volumes_negative.py
new file mode 100644
index 0000000..63b209e
--- /dev/null
+++ b/tempest/tests/volume/test_volumes_negative.py
@@ -0,0 +1,133 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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 nose.plugins.attrib import attr
+from nose.tools import raises
+
+from tempest import exceptions
+from tempest.common.utils.data_utils import rand_name
+from tempest.tests.volume.base import BaseVolumeTest
+
+
+class VolumesNegativeTest(BaseVolumeTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesNegativeTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_volume_get_nonexistant_volume_id(self):
+ """Should not be able to get a nonexistant volume"""
+ #Creating a nonexistant volume id
+ volume_id_list = []
+ resp, volumes = self.client.list_volumes()
+ for i in range(len(volumes)):
+ volume_id_list.append(volumes[i]['id'])
+ while True:
+ non_exist_id = rand_name('999')
+ if non_exist_id not in volume_id_list:
+ break
+ #Trying to Get a non existant volume
+ resp, volume = self.client.get_volume(non_exist_id)
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_volume_delete_nonexistant_volume_id(self):
+ """Should not be able to delete a nonexistant Volume"""
+ # Creating nonexistant volume id
+ volume_id_list = []
+ resp, volumes = self.client.list_volumes()
+ for i in range(len(volumes)):
+ volume_id_list.append(volumes[i]['id'])
+ while True:
+ non_exist_id = '12345678-abcd-4321-abcd-123456789098'
+ if non_exist_id not in volume_id_list:
+ break
+ # Try to Delete a non existant volume
+ resp, body = self.client.delete_volume(non_exist_id)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_volume_with_invalid_size(self):
+ """
+ Should not be able to create volume with invalid size
+ in request
+ """
+ v_name = rand_name('Volume-')
+ metadata = {'Type': 'work'}
+ resp, volume = self.client.create_volume(size='#$%',
+ display_name=v_name,
+ metadata=metadata)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_volume_with_out_passing_size(self):
+ """
+ Should not be able to create volume without passing size
+ in request
+ """
+ v_name = rand_name('Volume-')
+ metadata = {'Type': 'work'}
+ resp, volume = self.client.create_volume(size='',
+ display_name=v_name,
+ metadata=metadata)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_volume_with_size_zero(self):
+ """
+ Should not be able to create volume with size zero
+ """
+ v_name = rand_name('Volume-')
+ metadata = {'Type': 'work'}
+ resp, volume = self.client.create_volume(size='0',
+ display_name=v_name,
+ metadata=metadata)
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_invalid_volume_id(self):
+ """
+ Should not be able to get volume with invalid id
+ """
+ resp, volume = self.client.get_volume('#$%%&^&^')
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_volume_without_passing_volume_id(self):
+ """
+ Should not be able to get volume when empty ID is passed
+ """
+ resp, volume = self.client.get_volume('')
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_invalid_volume_id(self):
+ """
+ Should not be able to delete volume when invalid ID is passed
+ """
+ resp, volume = self.client.delete_volume('!@#$%^&*()')
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_volume_without_passing_volume_id(self):
+ """
+ Should not be able to delete volume when empty ID is passed
+ """
+ resp, volume = self.client.delete_volume('')
diff --git a/tools/pip-requires b/tools/pip-requires
index 2e1dc96..d3f9db7 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -3,3 +3,4 @@
httplib2>=0.7.0
pika
unittest2
+lxml