Adds Cinder client
This change adds Cinder client in services/volume * Keeps existing tests for Compute Extensions in tempest/tests/compute * Copies existing volumes test to tests/volume and refactors them to test the Cinder API, and adds a BaseVolumesTest class * Renames the Nova Extensions' volumes_client to volumes_extensions_client * Adds build_interval and build_timeout parameters in config for Cinder specific tests * Renames build_interval and build_timeout environment variables in the Devstack template file to COMPUTE_BUILD_INTERVAL and COMPUTE_BUILD_TIMEOUT * Adds volume specific environment variables VOLUME_BUILD_INTERVAL AND VOLUME_BUILD_TIMEOUT to the Devstack template file. Fixes LP Bug #1026190 Change-Id: I14d980ada1ddb29e8147f990aaf239fdcaae5eb6
This commit is contained in:
@@ -62,10 +62,10 @@ flavor_ref = 1
|
||||
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
|
||||
|
||||
@@ -125,7 +125,7 @@ bin_dir = /usr/local/bin
|
||||
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
|
||||
@@ -167,7 +167,10 @@ tenant_name = admin
|
||||
[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]
|
||||
@@ -181,3 +184,18 @@ username = admin
|
||||
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
|
||||
|
||||
@@ -58,12 +58,12 @@ flavor_ref = %FLAVOR_REF%
|
||||
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
|
||||
@@ -154,3 +154,18 @@ username = %IDENTITY_ADMIN_USERNAME%
|
||||
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%
|
||||
|
||||
@@ -208,7 +208,7 @@ class ComputeConfig(BaseConfig):
|
||||
|
||||
@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
|
||||
@@ -355,6 +355,30 @@ class NetworkConfig(BaseConfig):
|
||||
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 = {}
|
||||
@@ -402,6 +426,7 @@ class TempestConfig:
|
||||
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."""
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 +34,7 @@ from tempest.services.nova.json import extensions_client
|
||||
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 +46,7 @@ ExtensionsClient = extensions_client.ExtensionsClient
|
||||
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
|
||||
|
||||
@@ -199,6 +201,7 @@ class ComputeFuzzClientManager(FuzzClientManager):
|
||||
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)
|
||||
|
||||
@@ -22,6 +22,7 @@ from tempest import exceptions
|
||||
from tempest.services.image import service as image_service
|
||||
from tempest.services.network.json.network_client import NetworkClient
|
||||
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 @@ from tempest.services.nova.json.security_groups_client \
|
||||
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.flavors_client import FlavorsClientXML
|
||||
@@ -82,23 +84,24 @@ class Manager(object):
|
||||
|
||||
# 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)
|
||||
@@ -112,9 +115,10 @@ class Manager(object):
|
||||
self.extensions_client = ExtensionsClient(*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):
|
||||
|
||||
@@ -4,14 +4,15 @@ import json
|
||||
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"""
|
||||
0
tempest/services/volume/__init__.py
Normal file
0
tempest/services/volume/__init__.py
Normal file
0
tempest/services/volume/json/__init__.py
Normal file
0
tempest/services/volume/json/__init__.py
Normal file
120
tempest/services/volume/json/volumes_client.py
Normal file
120
tempest/services/volume/json/volumes_client.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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.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 = '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 the details of all volumes"""
|
||||
url = 'volumes/detail'
|
||||
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 get_volume(self, volume_id):
|
||||
"""Returns the details of a single volume"""
|
||||
url = "volumes/%s" % str(volume_id)
|
||||
resp, body = self.get(url)
|
||||
body = json.loads(body)
|
||||
return resp, body['volume']
|
||||
|
||||
def create_volume(self, size, **kwargs):
|
||||
"""
|
||||
Creates a new Volume.
|
||||
size(Required): Size of volume in GB.
|
||||
Following optional keyword arguments are accepted:
|
||||
display_name: Optional Volume Name.
|
||||
metadata: A dictionary of values to be used as metadata.
|
||||
"""
|
||||
post_body = {
|
||||
'size': size,
|
||||
'display_name': kwargs.get('display_name'),
|
||||
'metadata': kwargs.get('metadata'),
|
||||
}
|
||||
|
||||
post_body = json.dumps({'volume': post_body})
|
||||
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("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['display_name']
|
||||
volume_status = body['status']
|
||||
start = int(time.time())
|
||||
|
||||
while volume_status != status:
|
||||
time.sleep(self.build_interval)
|
||||
resp, body = self.get_volume(volume_id)
|
||||
volume_status = body['status']
|
||||
if volume_status == 'error':
|
||||
raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
|
||||
|
||||
if int(time.time()) - start >= self.build_timeout:
|
||||
message = 'Volume %s failed to reach %s status within '\
|
||||
'the required time (%s s).' % (volume_name, status,
|
||||
self.build_timeout)
|
||||
raise exceptions.TimeoutException(message)
|
||||
|
||||
def is_resource_deleted(self, id):
|
||||
try:
|
||||
self.get_volume(id)
|
||||
except exceptions.NotFound:
|
||||
return True
|
||||
return False
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
import nose
|
||||
|
||||
import unittest2 as unittest
|
||||
|
||||
@@ -60,6 +61,7 @@ class BaseComputeTest(unittest.TestCase):
|
||||
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
|
||||
|
||||
@@ -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 @@ class VolumesGetTest(BaseComputeTest):
|
||||
@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 @@ class VolumesGetTest(BaseComputeTest):
|
||||
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'])
|
||||
|
||||
@@ -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 @@ class VolumesTest(BaseComputeTest):
|
||||
# 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"""
|
||||
|
||||
@@ -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 @@ class VolumesNegativeTest(BaseComputeTest):
|
||||
@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):
|
||||
|
||||
0
tempest/tests/volume/__init__.py
Normal file
0
tempest/tests/volume/__init__.py
Normal file
149
tempest/tests/volume/base.py
Normal file
149
tempest/tests/volume/base.py
Normal file
@@ -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)
|
||||
99
tempest/tests/volume/test_volumes_get.py
Normal file
99
tempest/tests/volume/test_volumes_get.py
Normal file
@@ -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'])
|
||||
103
tempest/tests/volume/test_volumes_list.py
Normal file
103
tempest/tests/volume/test_volumes_list.py
Normal file
@@ -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))
|
||||
133
tempest/tests/volume/test_volumes_negative.py
Normal file
133
tempest/tests/volume/test_volumes_negative.py
Normal file
@@ -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('')
|
||||
Reference in New Issue
Block a user