Merge "Fix sample conf for compatability with devstack."
diff --git a/.gitignore b/.gitignore
index 061c2ff..b4dca86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,5 +8,6 @@
*.swo
*.egg-info
.tox
+.venv
dist
build
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index cfee55f..3cbe1b5 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -22,6 +22,8 @@
# Should typically be left as keystone unless you have a non-Keystone
# authentication API service
strategy = keystone
+# The identity region
+region = RegionOne
[compute]
# This section contains configuration options used when executing tests
@@ -54,6 +56,9 @@
# The above non-administrative user's tenant name
alt_tenant_name = alt_demo
+# The compute region
+region = RegionOne
+
# Reference data for tests. The ref and ref_alt should be
# distinct images/flavors.
image_ref = {$IMAGE_ID}
@@ -261,6 +266,10 @@
# this value as "object-store"
catalog_type = object-store
+# The object-store region
+region = RegionOne
+
+
[boto]
# This section contains configuration options used when executing tests
# with boto.
diff --git a/run_tests.sh b/run_tests.sh
index 7016a30..af33f7c 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -4,6 +4,10 @@
echo "Usage: $0 [OPTION]..."
echo "Run Tempest test suite"
echo ""
+ echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
+ echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
+ echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
+ echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -s, --smoke Only run smoke tests"
echo " -w, --whitebox Only run whitebox tests"
echo " -p, --pep8 Just run pep8"
@@ -15,6 +19,10 @@
function process_option {
case "$1" in
-h|--help) usage;;
+ -V|--virtual-env) always_venv=1; never_venv=0;;
+ -N|--no-virtual-env) always_venv=0; never_venv=1;;
+ -s|--no-site-packages) no_site_packages=1;;
+ -f|--force) force=1;;
-d|--debug) set -o xtrace;;
-p|--pep8) let just_pep8=1;;
-s|--smoke) noseargs="$noseargs --attr=type=smoke";;
@@ -25,6 +33,14 @@
noseargs=""
just_pep8=0
+venv=.venv
+with_venv=tools/with_venv.sh
+always_venv=0
+never_venv=0
+no_site_packages=0
+force=0
+wrapper=""
+
export NOSE_WITH_OPENSTACK=1
export NOSE_OPENSTACK_COLOR=1
@@ -37,6 +53,9 @@
process_option $arg
done
+if [ $no_site_packages -eq 1 ]; then
+ installvenvopts="--no-site-packages"
+fi
# only add tempest default if we don't specify a test
if [[ "x$noseargs" =~ "tempest" ]]; then
@@ -47,7 +66,7 @@
function run_tests {
- $NOSETESTS
+ ${wrapper} $NOSETESTS
}
function run_pep8 {
@@ -58,7 +77,7 @@
ignore='--ignore=N4,E121,E122,E125,E126'
- python tools/hacking.py ${ignore} ${srcfiles}
+ ${wrapper} python tools/hacking.py ${ignore} ${srcfiles}
}
NOSETESTS="nosetests $noseargs"
@@ -68,6 +87,32 @@
exit
fi
+if [ $never_venv -eq 0 ]
+then
+ # Remove the virtual environment if --force used
+ if [ $force -eq 1 ]; then
+ echo "Cleaning virtualenv..."
+ rm -rf ${venv}
+ fi
+ if [ -e ${venv} ]; then
+ wrapper="${with_venv}"
+ else
+ if [ $always_venv -eq 1 ]; then
+ # Automatically install the virtualenv
+ python tools/install_venv.py $installvenvopts
+ wrapper="${with_venv}"
+ else
+ echo -e "No virtual environment found...create one? (Y/n) \c"
+ read use_ve
+ if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
+ # Install the virtualenv and run the test suite in it
+ python tools/install_venv.py $installvenvopts
+ wrapper=${with_venv}
+ fi
+ fi
+ fi
+fi
+
run_tests || exit
if [ -z "$noseargs" ]; then
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index cb18a9c..884d147 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -44,7 +44,7 @@
self.token = None
self.base_url = None
self.config = config
- self.region = 0
+ self.region = {'compute': self.config.compute.region}
self.endpoint_url = 'publicURL'
self.strategy = self.config.identity.strategy
self.headers = {'Content-Type': 'application/%s' % self.TYPE,
@@ -142,7 +142,12 @@
mgmt_url = None
for ep in auth_data['serviceCatalog']:
if ep["type"] == service:
- mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
+ for _ep in ep['endpoints']:
+ if service in self.region and \
+ _ep['region'] == self.region[service]:
+ mgmt_url = _ep[self.endpoint_url]
+ if not mgmt_url:
+ mgmt_url = ep['endpoints'][0][self.endpoint_url]
tenant_id = auth_data['token']['tenant']['id']
break
diff --git a/tempest/config.py b/tempest/config.py
index 1d1aa49..fc1bd74 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -96,6 +96,11 @@
"""Which auth method does the environment use? (basic|keystone)"""
return self.get("strategy", 'keystone')
+ @property
+ def region(self):
+ """The identity region name to use."""
+ return self.get("region")
+
class IdentityAdminConfig(BaseConfig):
@@ -173,6 +178,11 @@
return self.get("alt_password")
@property
+ def region(self):
+ """The compute region name to use."""
+ return self.get("region")
+
+ @property
def image_ref(self):
"""Valid primary image to use in tests."""
return self.get("image_ref", "{$IMAGE_ID}")
@@ -473,6 +483,11 @@
"""Catalog type of the Object-Storage service."""
return self.get("catalog_type", 'object-store')
+ @property
+ def region(self):
+ """The object-store region name to use."""
+ return self.get("region")
+
class BotoConfig(BaseConfig):
"""Provides configuration information for connecting to EC2/S3."""
diff --git a/tempest/openstack.py b/tempest/openstack.py
index e29ea8e..01b4cc1 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -22,33 +22,33 @@
from tempest.services.boto.clients import APIClientEC2
from tempest.services.boto.clients import ObjectClientS3
from tempest.services.compute.json.extensions_client import \
- ExtensionsClientJSON
+ ExtensionsClientJSON
from tempest.services.compute.json.flavors_client import FlavorsClientJSON
from tempest.services.compute.json.floating_ips_client import \
- FloatingIPsClientJSON
+ FloatingIPsClientJSON
from tempest.services.compute.json.images_client import ImagesClientJSON
from tempest.services.compute.json.limits_client import LimitsClientJSON
from tempest.services.compute.json.servers_client import ServersClientJSON
from tempest.services.compute.json.security_groups_client import \
- SecurityGroupsClientJSON
+ SecurityGroupsClientJSON
from tempest.services.compute.json.keypairs_client import KeyPairsClientJSON
from tempest.services.compute.json.quotas_client import QuotasClient
from tempest.services.compute.json.volumes_extensions_client import \
- VolumesExtensionsClientJSON
+ VolumesExtensionsClientJSON
from tempest.services.compute.json.console_output_client import \
- ConsoleOutputsClient
+ ConsoleOutputsClient
from tempest.services.compute.xml.extensions_client import ExtensionsClientXML
from tempest.services.compute.xml.flavors_client import FlavorsClientXML
from tempest.services.compute.xml.floating_ips_client import \
- FloatingIPsClientXML
+ FloatingIPsClientXML
from tempest.services.compute.xml.images_client import ImagesClientXML
from tempest.services.compute.xml.keypairs_client import KeyPairsClientXML
from tempest.services.compute.xml.limits_client import LimitsClientXML
from tempest.services.compute.xml.security_groups_client \
-import SecurityGroupsClientXML
+ import SecurityGroupsClientXML
from tempest.services.compute.xml.servers_client import ServersClientXML
from tempest.services.compute.xml.volumes_extensions_client import \
- VolumesExtensionsClientXML
+ VolumesExtensionsClientXML
from tempest.services.identity.json.admin_client import AdminClientJSON
from tempest.services.identity.json.admin_client import TokenClientJSON
from tempest.services.identity.xml.admin_client import AdminClientXML
@@ -60,6 +60,8 @@
from tempest.services.object_storage.object_client import ObjectClient
from tempest.services.volume.json.volumes_client import VolumesClientJSON
from tempest.services.volume.xml.volumes_client import VolumesClientXML
+from tempest.services.object_storage.object_client import \
+ ObjectClientCustomizedHeader
LOG = logging.getLogger(__name__)
@@ -191,6 +193,7 @@
self.object_client = ObjectClient(*client_args)
self.ec2api_client = APIClientEC2(*client_args)
self.s3_client = ObjectClientS3(*client_args)
+ self.custom_object_client = ObjectClientCustomizedHeader(*client_args)
class AltManager(Manager):
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index c8f63ef..5bf8de0 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import httplib2
+import json
import re
from tempest.common.rest_client import RestClient
@@ -116,3 +118,44 @@
resp, body = self.copy(url, headers=headers)
return resp, body
+
+
+class ObjectClientCustomizedHeader(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ObjectClientCustomizedHeader, self).__init__(config, username,
+ password, auth_url,
+ tenant_name)
+ #Overwrites json-specific header encoding in RestClient
+ self.service = self.config.object_storage.catalog_type
+ self.format = 'json'
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """A simple HTTP request interface."""
+ self.http_obj = httplib2.Http()
+ if headers is None:
+ headers = {}
+ if self.base_url is None:
+ self._set_auth()
+
+ req_url = "%s/%s" % (self.base_url, url)
+ resp, resp_body = self.http_obj.request(req_url, method,
+ headers=headers, body=body)
+ if resp.status in (401, 403):
+ try:
+ resp_body = json.loads(resp_body)
+ raise exceptions.Unauthorized(resp_body['error']['message'])
+ except ValueError:
+ pass
+ return resp, resp_body
+
+ def get_object(self, container, object_name, metadata=None):
+ """Retrieve object's data."""
+ headers = {}
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
+ url = "{0}/{1}".format(container, object_name)
+ resp, body = self.get(url, headers=headers)
+ return resp, body
diff --git a/tempest/services/volume/xml/admin/__init__.py b/tempest/services/volume/xml/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/xml/admin/__init__.py
diff --git a/tempest/services/volume/xml/admin/volume_types_client.py b/tempest/services/volume/xml/admin/volume_types_client.py
new file mode 100644
index 0000000..3da1af0
--- /dev/null
+++ b/tempest/services/volume/xml/admin/volume_types_client.py
@@ -0,0 +1,195 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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 urllib
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import exceptions
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+
+class VolumeTypesClientXML(RestClientXML):
+ """
+ Client class to send CRUD Volume Types API requests to a Cinder endpoint
+ """
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(VolumeTypesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.volume.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_volume_type(self, body):
+ vol_type = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ if tag == 'extra_specs':
+ vol_type['extra_specs'] = dict((meta.get('key'),
+ meta.text)
+ for meta in list(child))
+ else:
+ vol_type[tag] = xml_to_json(child)
+ return vol_type
+
+ def _parse_volume_type_extra_specs(self, body):
+ extra_spec = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ else:
+ extra_spec[tag] = xml_to_json(child)
+ return extra_spec
+
+ def list_volume_types(self, params=None):
+ """List all the volume_types created"""
+ url = 'types'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volume_types = []
+ if body is not None:
+ volume_types += [self._parse_volume_type(vol)
+ for vol in list(body)]
+ return resp, volume_types
+
+ def get_volume_type(self, type_id):
+ """Returns the details of a single volume_type"""
+ url = "types/%s" % str(type_id)
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume_type(body)
+
+ def create_volume_type(self, name, **kwargs):
+ """
+ Creates a new Volume_type.
+ name(Required): Name of volume_type.
+ Following optional keyword arguments are accepted:
+ extra_specs: A dictionary of values to be used as extra_specs.
+ """
+ vol_type = Element("volume_type", xmlns=XMLNS_11)
+ if name:
+ vol_type.add_attr('name', name)
+
+ extra_specs = kwargs.get('extra_specs')
+ if extra_specs:
+ _extra_specs = Element('extra_specs')
+ vol_type.append(_extra_specs)
+ for key, value in extra_specs.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ _extra_specs.append(spec)
+
+ resp, body = self.post('types', str(Document(vol_type)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume_type(self, type_id):
+ """Deletes the Specified Volume_type"""
+ return self.delete("types/%s" % str(type_id))
+
+ def list_volume_types_extra_specs(self, vol_type_id, params=None):
+ """List all the volume_types extra specs created"""
+ url = 'types/%s/extra_specs' % str(vol_type_id)
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ extra_specs = []
+ if body is not None:
+ extra_specs += [self._parse_volume_type_extra_specs(spec)
+ for spec in list(body)]
+ return resp, extra_specs
+
+ def get_volume_type_extra_specs(self, vol_type_id, extra_spec_name):
+ """Returns the details of a single volume_type extra spec"""
+ url = "types/%s/extra_specs/%s" % (str(vol_type_id),
+ str(extra_spec_name))
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume_type_extra_specs(body)
+
+ def create_volume_type_extra_specs(self, vol_type_id, extra_spec):
+ """
+ Creates a new Volume_type extra spec.
+ vol_type_id: Id of volume_type.
+ extra_specs: A dictionary of values to be used as extra_specs.
+ """
+ url = "types/%s/extra_specs" % str(vol_type_id)
+ extra_specs = Element("extra_specs", xmlns=XMLNS_11)
+ if extra_spec:
+ for key, value in extra_spec.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ extra_specs.append(spec)
+
+ resp, body = self.post(url, str(Document(extra_specs)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume_type_extra_specs(self, vol_id, extra_spec_name):
+ """Deletes the Specified Volume_type extra spec"""
+ return self.delete("types/%s/extra_specs/%s" % ((str(vol_id)),
+ str(extra_spec_name)))
+
+ def update_volume_type_extra_specs(self, vol_type_id, extra_spec_name,
+ extra_spec):
+ """
+ Update a volume_type extra spec.
+ vol_type_id: Id of volume_type.
+ extra_spec_name: Name of the extra spec to be updated.
+ extra_spec: A dictionary of with key as extra_spec_name and the
+ updated value.
+ """
+ url = "types/%s/extra_specs/%s" % (str(vol_type_id),
+ str(extra_spec_name))
+ extra_specs = Element("extra_specs", xmlns=XMLNS_11)
+ for key, value in extra_spec.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ extra_specs.append(spec)
+ resp, body = self.put(url, str(Document(extra_specs)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume_type(id)
+ except exceptions.NotFound:
+ return True
+ return False
diff --git a/tempest/tests/object_storage/base.py b/tempest/tests/object_storage/base.py
index 8edb3d2..f29798f 100644
--- a/tempest/tests/object_storage/base.py
+++ b/tempest/tests/object_storage/base.py
@@ -32,6 +32,7 @@
cls.container_client = cls.os.container_client
cls.account_client = cls.os.account_client
cls.config = cls.os.config
+ cls.custom_object_client = cls.os.custom_object_client
try:
cls.account_client.list_account_containers()
diff --git a/tempest/tests/object_storage/test_object_expiry.py b/tempest/tests/object_storage/test_object_expiry.py
new file mode 100644
index 0000000..8437d55
--- /dev/null
+++ b/tempest/tests/object_storage/test_object_expiry.py
@@ -0,0 +1,93 @@
+# 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 arbitrary_string
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.tests.object_storage import base
+from time import sleep
+import unittest2 as unittest
+
+
+class ObjectExpiryTest(base.BaseObjectTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ObjectExpiryTest, cls).setUpClass()
+
+ #Create a container
+ cls.container_name = rand_name(name='TestContainer')
+ cls.container_client.create_container(cls.container_name)
+
+ @classmethod
+ def tearDownClass(cls):
+ """The test script fails in tear down class
+ as the container contains expired objects (LP bug 1069849).
+ But delete action for the expired object is raising
+ NotFound exception and also non empty container cannot be deleted."""
+
+ #Get list of all object in the container
+ objlist = \
+ cls.container_client.list_all_container_objects(cls.container_name)
+
+ #Attempt to delete every object in the container
+ if objlist:
+ for obj in objlist:
+ resp, _ = cls.object_client.delete_object(cls.container_name,
+ obj['name'])
+
+ #Attempt to delete the container
+ resp, _ = cls.container_client.delete_container(cls.container_name)
+
+ @unittest.skip('Until bug 1069849 is resolved.')
+ @attr(type='regression')
+ def test_get_object_after_expiry_time(self):
+ """GET object after expiry time"""
+ #TODO(harika-vakadi): Similar test case has to be created for
+ # "X-Delete-At", after this test case works.
+
+ #Create Object
+ object_name = rand_name(name='TestObject')
+ data = arbitrary_string()
+ resp, _ = self.object_client.create_object(self.container_name,
+ object_name, data)
+
+ #Update object metadata with expiry time of 3 seconds
+ metadata = {'X-Delete-After': '3'}
+ resp, _ = \
+ self.object_client.update_object_metadata(self.container_name,
+ object_name, metadata,
+ metadata_prefix='')
+
+ resp, _ = \
+ self.object_client.list_object_metadata(self.container_name,
+ object_name)
+
+ self.assertEqual(resp['status'], '200')
+ self.assertIn('x-delete-at', resp)
+
+ resp, body = self.object_client.get_object(self.container_name,
+ object_name)
+ self.assertEqual(resp['status'], '200')
+ # Check data
+ self.assertEqual(body, data)
+ # Sleep for over 5 seconds, so that object is expired
+ sleep(5)
+ # Verification of raised exception after object gets expired
+ self.assertRaises(exceptions.NotFound, self.object_client.get_object,
+ self.container_name, object_name)
diff --git a/tempest/tests/object_storage/test_object_services.py b/tempest/tests/object_storage/test_object_services.py
index 3be2bee..807a99e 100644
--- a/tempest/tests/object_storage/test_object_services.py
+++ b/tempest/tests/object_storage/test_object_services.py
@@ -263,3 +263,36 @@
src_container_name)
resp, _ = self.container_client.delete_container(
dst_container_name)
+
+ @attr(type='smoke')
+ def test_access_object_without_using_creds(self):
+ """Make container public-readable, and access the object
+ anonymously, e.g. without using credentials"""
+
+ # Update Container Metadata to make public readable
+ headers = {'X-Container-Read': '.r:*,.rlistings'}
+ resp, body = \
+ self.container_client.update_container_metadata(
+ self.container_name, metadata=headers, metadata_prefix='')
+ self.assertIn(resp['status'], '204')
+
+ # Create Object
+ object_name = rand_name(name='Object')
+ data = arbitrary_string(size=len(object_name) * 1,
+ base_text=object_name)
+ resp, _ = self.object_client.create_object(self.container_name,
+ object_name, data)
+ self.assertEqual(resp['status'], '201')
+
+ # List container metadata
+ resp, _ = \
+ self.container_client.list_container_metadata(self.container_name)
+ self.assertEqual(resp['status'], '204')
+ self.assertIn('x-container-read', resp)
+ self.assertEqual(resp['x-container-read'], '.r:*,.rlistings')
+
+ # Trying to Get Object with empty Headers as it is public readable
+ resp, body = \
+ self.custom_object_client.get_object(self.container_name,
+ object_name, metadata={})
+ self.assertEqual(body, data)
diff --git a/tempest/tests/volume/admin/base.py b/tempest/tests/volume/admin/base.py
new file mode 100644
index 0000000..420da42
--- /dev/null
+++ b/tempest/tests/volume/admin/base.py
@@ -0,0 +1,67 @@
+# 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 tempest import config
+import tempest.services.volume.json.admin.volume_types_client \
+ as volume_types_json_client
+import tempest.services.volume.xml.admin.volume_types_client \
+ as volume_types_xml_client
+from tempest.tests.volume.base import BaseVolumeTest
+
+
+class BaseVolumeAdminTest(BaseVolumeTest):
+ """Base test case class for all Volume Admin API tests"""
+ @classmethod
+ def setUpClass(cls):
+ super(BaseVolumeAdminTest, cls).setUpClass()
+ cls.config = config.TempestConfig()
+ cls.adm_user = cls.config.compute_admin.username
+ cls.adm_pass = cls.config.compute_admin.password
+ cls.adm_tenant = cls.config.compute_admin.tenant_name
+ cls.auth_url = cls.config.identity.auth_url
+
+ if not cls.adm_user and cls.adm_pass and cls.adm_tenant:
+ msg = ("Missing Volume Admin API credentials "
+ "in configuration.")
+ raise nose.SkipTest(msg)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(BaseVolumeAdminTest, cls).tearDownClass()
+
+
+class BaseVolumeAdminTestJSON(BaseVolumeAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(BaseVolumeAdminTestJSON, cls).setUpClass()
+ cls.client = volume_types_json_client.\
+ VolumeTypesClientJSON(cls.config, cls.adm_user, cls.adm_pass,
+ cls.auth_url, cls.adm_tenant)
+
+
+class BaseVolumeAdminTestXML(BaseVolumeAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(BaseVolumeAdminTestXML, cls).setUpClass()
+ cls.client = volume_types_xml_client.\
+ VolumeTypesClientXML(cls.config, cls.adm_user, cls.adm_pass,
+ cls.auth_url, cls.adm_tenant)
diff --git a/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
new file mode 100644
index 0000000..bfbfaae
--- /dev/null
+++ b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
@@ -0,0 +1,168 @@
+# 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 unittest
+import uuid
+
+from nose.plugins.attrib import attr
+from nose.tools import raises
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestJSON
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestXML
+
+
+class ExtraSpecsNegativeTestBase():
+
+ @staticmethod
+ def setUpClass(cls):
+ cls.client = cls.client
+ vol_type_name = rand_name('Volume-type-')
+ cls.extra_specs = {"spec1": "val1"}
+ resp, cls.volume_type = cls.client.create_volume_type(vol_type_name,
+ extra_specs=
+ cls.extra_specs)
+
+ @staticmethod
+ def tearDownClass(cls):
+ cls.client.delete_volume_type(cls.volume_type['id'])
+
+ @unittest.skip('Until bug 1090320 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_no_body(self):
+ """ Should not update volume type extra specs with no body"""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ extra_spec.keys()[0],
+ None)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_nonexistent_extra_spec_id(self):
+ """ Should not update volume type extra specs with nonexistent id."""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ str(uuid.uuid4()),
+ extra_spec)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_none_extra_spec_id(self):
+ """ Should not update volume type extra specs with none id."""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ None, extra_spec)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_multiple_extra_spec(self):
+ """ Should not update volume type extra specs with multiple specs as
+ body.
+ """
+ extra_spec = {"spec1": "val2", 'spec2': 'val1'}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ extra_spec.keys()[0],
+ extra_spec)
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_create_nonexistent_type_id(self):
+ """ Should not create volume type extra spec for nonexistent volume
+ type id.
+ """
+ extra_specs = {"spec2": "val1"}
+ self.client.create_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs)
+
+ @unittest.skip('Until bug 1090322 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_none_body(self):
+ """ Should not create volume type extra spec for none POST body."""
+ self.client.create_volume_type_extra_specs(self.volume_type['id'],
+ None)
+
+ @unittest.skip('Until bug 1090322 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_invalid_body(self):
+ """ Should not create volume type extra spec for invalid POST body."""
+ self.client.create_volume_type_extra_specs(self.volume_type['id'],
+ ['invalid'])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_nonexistent_volume_type_id(self):
+ """ Should not delete volume type extra spec for nonexistent
+ type id.
+ """
+ extra_specs = {"spec1": "val1"}
+ self.client.delete_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs.keys()[0])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_list_nonexistent_volume_type_id(self):
+ """ Should not list volume type extra spec for nonexistent type id."""
+ self.client.list_volume_types_extra_specs(str(uuid.uuid4()))
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_volume_type_id(self):
+ """ Should not get volume type extra spec for nonexistent type id."""
+ extra_specs = {"spec1": "val1"}
+ self.client.get_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs.keys()[0])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_extra_spec_id(self):
+ """ Should not get volume type extra spec for nonexistent extra spec
+ id.
+ """
+ self.client.get_volume_type_extra_specs(self.volume_type['id'],
+ str(uuid.uuid4()))
+
+
+class ExtraSpecsNegativeTestXML(BaseVolumeAdminTestXML,
+ ExtraSpecsNegativeTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ExtraSpecsNegativeTestXML, cls).setUpClass()
+ ExtraSpecsNegativeTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(ExtraSpecsNegativeTestXML, cls).tearDownClass()
+ ExtraSpecsNegativeTestBase.tearDownClass(cls)
+
+
+class ExtraSpecsNegativeTestJSON(BaseVolumeAdminTestJSON,
+ ExtraSpecsNegativeTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ExtraSpecsNegativeTestJSON, cls).setUpClass()
+ ExtraSpecsNegativeTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(ExtraSpecsNegativeTestJSON, cls).tearDownClass()
+ ExtraSpecsNegativeTestBase.tearDownClass(cls)
diff --git a/tempest/tests/volume/admin/test_volume_types_negative.py b/tempest/tests/volume/admin/test_volume_types_negative.py
new file mode 100644
index 0000000..91237fc
--- /dev/null
+++ b/tempest/tests/volume/admin/test_volume_types_negative.py
@@ -0,0 +1,78 @@
+# 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 unittest
+import uuid
+
+from nose.plugins.attrib import attr
+from nose.tools import raises
+
+from tempest import exceptions
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestJSON
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestXML
+
+
+class VolumeTypesNegativeTestBase():
+
+ @staticmethod
+ def setUpClass(cls):
+ cls.client = cls.client
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_create_with_nonexistent_volume_type(self):
+ """ Should not be able to create volume with nonexistent volume_type.
+ """
+ self.volumes_client.create_volume(size=1,
+ display_name=str(uuid.uuid4()),
+ volume_type=str(uuid.uuid4()))
+
+ @unittest.skip('Until bug 1090356 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_with_empty_name(self):
+ """ Should not be able to create volume type with an empty name."""
+ self.client.create_volume_type('')
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_type_id(self):
+ """ Should not be able to get volume type with nonexistent type id."""
+ self.client.get_volume_type(str(uuid.uuid4()))
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_nonexistent_type_id(self):
+ """ Should not be able to delete volume type with nonexistent type id.
+ """
+ self.client.delete_volume_type(str(uuid.uuid4()))
+
+
+class VolumesTypesNegativeTestXML(BaseVolumeAdminTestXML,
+ VolumeTypesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesTypesNegativeTestXML, cls).setUpClass()
+ VolumeTypesNegativeTestBase.setUpClass(cls)
+
+
+class VolumesTypesNegativeTestJSON(BaseVolumeAdminTestJSON,
+ VolumeTypesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesTypesNegativeTestJSON, cls).setUpClass()
+ VolumeTypesNegativeTestBase.setUpClass(cls)
diff --git a/tools/install_venv.py b/tools/install_venv.py
new file mode 100644
index 0000000..42ed32c
--- /dev/null
+++ b/tools/install_venv.py
@@ -0,0 +1,248 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Copyright 2010 OpenStack, LLC
+#
+# 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.
+
+"""Installation script for Tempest's development virtualenv."""
+
+import optparse
+import os
+import subprocess
+import sys
+
+
+ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+VENV = os.path.join(ROOT, '.venv')
+PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
+TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
+PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
+
+
+def die(message, *args):
+ print >> sys.stderr, message % args
+ sys.exit(1)
+
+
+def check_python_version():
+ if sys.version_info < (2, 6):
+ die("Need Python Version >= 2.6")
+
+
+def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
+ """Runs a command in an out-of-process shell.
+
+ Returns the output of that command. Working directory is ROOT.
+ """
+ if redirect_output:
+ stdout = subprocess.PIPE
+ else:
+ stdout = None
+
+ proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
+ output = proc.communicate()[0]
+ if check_exit_code and proc.returncode != 0:
+ die('Command "%s" failed.\n%s', ' '.join(cmd), output)
+ return (output, proc.returncode)
+
+
+def run_command(cmd, redirect_output=True, check_exit_code=True):
+ return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
+
+
+class Distro(object):
+
+ def check_cmd(self, cmd):
+ return bool(run_command(['which', cmd], check_exit_code=False).strip())
+
+ def install_virtualenv(self):
+ if self.check_cmd('virtualenv'):
+ return
+
+ if self.check_cmd('easy_install'):
+ print 'Installing virtualenv via easy_install...',
+ if run_command(['easy_install', 'virtualenv']):
+ print 'Succeeded'
+ return
+ else:
+ print 'Failed'
+
+ die('ERROR: virtualenv not found.\n\nTempest development'
+ ' requires virtualenv, please install it using your'
+ ' favorite package management tool')
+
+ def post_process(self):
+ """Any distribution-specific post-processing gets done here.
+
+ In particular, this is useful for applying patches to code inside
+ the venv.
+ """
+ pass
+
+
+class Fedora(Distro):
+ """This covers all Fedora-based distributions.
+
+ Includes: Fedora, RHEL, CentOS, Scientific Linux"""
+
+ def check_pkg(self, pkg):
+ return run_command_with_code(['rpm', '-q', pkg],
+ check_exit_code=False)[1] == 0
+
+ def yum_install(self, pkg, **kwargs):
+ print "Attempting to install '%s' via yum" % pkg
+ run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
+
+ def apply_patch(self, originalfile, patchfile):
+ run_command(['patch', originalfile, patchfile])
+
+ def install_virtualenv(self):
+ if self.check_cmd('virtualenv'):
+ return
+
+ if not self.check_pkg('python-virtualenv'):
+ self.yum_install('python-virtualenv', check_exit_code=False)
+
+ super(Fedora, self).install_virtualenv()
+
+ def post_process(self):
+ """Workaround for a bug in eventlet.
+
+ This currently affects RHEL6.1, but the fix can safely be
+ applied to all RHEL and Fedora distributions.
+
+ This can be removed when the fix is applied upstream.
+
+ Nova: https://bugs.launchpad.net/nova/+bug/884915
+ Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
+ """
+
+ # Install "patch" program if it's not there
+ if not self.check_pkg('patch'):
+ self.yum_install('patch')
+
+ # Apply the eventlet patch
+ self.apply_patch(os.path.join(VENV, 'lib', PY_VERSION, 'site-packages',
+ 'eventlet/green/subprocess.py'),
+ 'contrib/redhat-eventlet.patch')
+
+
+def get_distro():
+ if (os.path.exists('/etc/fedora-release') or
+ os.path.exists('/etc/redhat-release')):
+ return Fedora()
+ else:
+ return Distro()
+
+
+def check_dependencies():
+ get_distro().install_virtualenv()
+
+
+def create_virtualenv(venv=VENV, no_site_packages=True):
+ """Creates the virtual environment and installs PIP.
+
+ Creates the virtual environment and installs PIP only into the
+ virtual environment.
+ """
+ print 'Creating venv...',
+ if no_site_packages:
+ run_command(['virtualenv', '-q', '--no-site-packages', VENV])
+ else:
+ run_command(['virtualenv', '-q', VENV])
+ print 'done.'
+ print 'Installing pip in virtualenv...',
+ if not run_command(['tools/with_venv.sh', 'easy_install',
+ 'pip>1.0']).strip():
+ die("Failed to install pip.")
+ print 'done.'
+
+
+def pip_install(*args):
+ run_command(['tools/with_venv.sh',
+ 'pip', 'install', '--upgrade'] + list(args),
+ redirect_output=False)
+
+
+def install_dependencies(venv=VENV):
+ print 'Installing dependencies with pip (this can take a while)...'
+
+ # First things first, make sure our venv has the latest pip and distribute.
+ # NOTE: we keep pip at version 1.1 since the most recent version causes
+ # the .venv creation to fail. See:
+ # https://bugs.launchpad.net/nova/+bug/1047120
+ pip_install('pip==1.1')
+ pip_install('distribute')
+
+ # Install greenlet by hand - just listing it in the requires file does not
+ # get it in stalled in the right order
+ pip_install('greenlet')
+
+ pip_install('-r', PIP_REQUIRES)
+ pip_install('-r', TEST_REQUIRES)
+
+ # Install tempest into the virtual_env. No more path munging!
+ run_command([os.path.join(venv, 'bin/python'), 'setup.py', 'develop'])
+
+
+def post_process():
+ get_distro().post_process()
+
+
+def print_help():
+ help = """
+ Tempest development environment setup is complete.
+
+ Tempest development uses virtualenv to track and manage Python dependencies
+ while in development and testing.
+
+ To activate the Tempest virtualenv for the extent of your current shell
+ session you can run:
+
+ $ source .venv/bin/activate
+
+ Or, if you prefer, you can run commands in the virtualenv on a case by case
+ basis by running:
+
+ $ tools/with_venv.sh <your command>
+
+ Also, make test will automatically use the virtualenv.
+ """
+ print help
+
+
+def parse_args():
+ """Parses command-line arguments."""
+ parser = optparse.OptionParser()
+ parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
+ default=False, action="store_true",
+ help="Do not inherit packages from global Python"
+ " install")
+ return parser.parse_args()
+
+
+def main(argv):
+ (options, args) = parse_args()
+ check_python_version()
+ check_dependencies()
+ create_virtualenv(no_site_packages=options.no_site_packages)
+ install_dependencies()
+ post_process()
+ print_help()
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/tools/pip-requires b/tools/pip-requires
index 7877906..6542a3c 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -6,3 +6,6 @@
boto>=2.2.1
paramiko
netaddr
+python-glanceclient>=0.5.0,<2
+python-keystoneclient>=0.2.0
+python-novaclient>=2.10.0,<3
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
index 2e2b855..550c477 100755
--- a/tools/with_venv.sh
+++ b/tools/with_venv.sh
@@ -1,4 +1,4 @@
#!/bin/bash
TOOLS=`dirname $0`
-VENV=$TOOLS/../.kong-venv
-source $VENV/bin/activate && $@
+VENV=$TOOLS/../.venv
+source $VENV/bin/activate && "$@"