Adds Images API tests
* Adds new images API tests in tempest/tests/images
* Adds tempest.openstack.ServiceManager class to deal with non-Compute API
clients
* Adds an [image] section to the configuration file and manager
that deals with Image API stuff
* Updates the tools/conf_from_devstack script to write an image
section to the generated tempest.conf
This is all in preparation for removing the functional integration
tests from Glance and putting them in Tempest...
Change-Id: I6caf50e5cab97794204472151acc88fcdd0fc224
diff --git a/tempest/config.py b/tempest/config.py
index 0d5a096..6f816d2 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -136,6 +136,62 @@
return self.get("authentication", 'keystone')
+class ImagesConfig(object):
+ """
+ Provides configuration information for connecting to an
+ OpenStack Images service.
+ """
+
+ def __init__(self, conf):
+ self.conf = conf
+
+ def get(self, item_name, default_value=None):
+ try:
+ return self.conf.get("image", item_name)
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ return default_value
+
+ @property
+ def host(self):
+ """Host IP for making Images API requests. Defaults to '127.0.0.1'."""
+ return self.get("host", "127.0.0.1")
+
+ @property
+ def port(self):
+ """Listen port of the Images service."""
+ return int(self.get("port", "9292"))
+
+ @property
+ def api_version(self):
+ """Version of the API"""
+ return self.get("api_version", "1")
+
+ @property
+ def username(self):
+ """Username to use for Images API requests. Defaults to 'admin'."""
+ return self.get("user", "admin")
+
+ @property
+ def password(self):
+ """Password for user"""
+ return self.get("password", "")
+
+ @property
+ def tenant(self):
+ """Tenant to use for Images API requests. Defaults to 'admin'."""
+ return self.get("tenant", "admin")
+
+ @property
+ def service_token(self):
+ """Token to use in querying the API. Default: None"""
+ return self.get("service_token")
+
+ @property
+ def auth_url(self):
+ """Optional URL to auth service. Will be discovered if None"""
+ return self.get("auth_url")
+
+
class TempestConfig(object):
"""Provides OpenStack configuration information."""
@@ -165,6 +221,7 @@
self._conf = self.load_config(path)
self.nova = NovaConfig(self._conf)
self.env = EnvironmentConfig(self._conf)
+ self.images = ImagesConfig(self._conf)
def load_config(self, path):
"""Read configuration from given path and return a config object."""
diff --git a/tempest/openstack.py b/tempest/openstack.py
index af7ee84..ba32487 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -1,3 +1,5 @@
+import tempest.config
+from tempest.services.image import service as image_service
from tempest.services.nova.json.images_client import ImagesClient
from tempest.services.nova.json.flavors_client import FlavorsClient
from tempest.services.nova.json.servers_client import ServersClient
@@ -74,3 +76,16 @@
self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url)
+
+
+class ServiceManager(object):
+
+ """
+ Top-level object housing clients for OpenStack APIs
+ """
+
+ def __init__(self):
+ self.config = tempest.config.TempestConfig()
+ self.services = {}
+ self.services['image'] = image_service.Service(self.config)
+ self.images = self.services['image']
diff --git a/tempest/services/__init__.py b/tempest/services/__init__.py
index e69de29..b2fdc5f 100644
--- a/tempest/services/__init__.py
+++ b/tempest/services/__init__.py
@@ -0,0 +1,39 @@
+# 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.
+
+"""
+Base Service class, which acts as a descriptor for an OpenStack service
+in the test environment
+"""
+
+
+class Service(object):
+
+ def __init__(self, config):
+ """
+ Initializes the service.
+
+ :param config: `tempest.config.Config` object
+ """
+ self.config = config
+
+ def get_client(self):
+ """
+ Returns a client object that may be used to query
+ the service API.
+ """
+ raise NotImplementedError
diff --git a/tempest/services/image/__init__.py b/tempest/services/image/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/image/__init__.py
diff --git a/tempest/services/image/service.py b/tempest/services/image/service.py
new file mode 100644
index 0000000..efeacb3
--- /dev/null
+++ b/tempest/services/image/service.py
@@ -0,0 +1,62 @@
+# 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.
+
+"""
+Image Service class, which acts as a descriptor for the OpenStack Images
+service running in the test environment.
+"""
+
+from tempest.services import Service as BaseService
+
+
+class Service(BaseService):
+
+ def __init__(self, config):
+ """
+ Initializes the service.
+
+ :param config: `tempest.config.Config` object
+ """
+ self.config = config
+
+ # Determine the Images API version
+ self.api_version = int(config.images.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,
+ 'auth_url': config.images.auth_url,
+ 'strategy': 'keystone'
+ }
+ service_token = config.images.service_token
+ self._client = client.Client(config.images.host,
+ config.images.port,
+ auth_tok=service_token)
+ else:
+ raise NotImplementedError
+
+ def get_client(self):
+ """
+ Returns a client object that may be used to query
+ the service API.
+ """
+ assert self._client
+ return self._client
diff --git a/tempest/tests/image/__init__.py b/tempest/tests/image/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/image/__init__.py
diff --git a/tempest/tests/image/test_images.py b/tempest/tests/image/test_images.py
new file mode 100644
index 0000000..8567b51
--- /dev/null
+++ b/tempest/tests/image/test_images.py
@@ -0,0 +1,202 @@
+# 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 cStringIO as StringIO
+import random
+
+import unittest2 as unittest
+
+from nose.plugins.attrib import attr
+from nose.plugins.skip import SkipTest
+
+GLANCE_INSTALLED = False
+try:
+ from glance import client
+ from glance.common import exception
+ GLANCE_INSTALLED = True
+except ImportError:
+ pass
+
+from tempest import openstack
+
+
+@unittest.skipUnless(GLANCE_INSTALLED, 'Glance not installed')
+class CreateRegisterImagesTest(unittest.TestCase):
+
+ """
+ Here we test the registration and creation of images
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ cls.os = openstack.ServiceManager()
+ cls.client = cls.os.images.get_client()
+ cls.created_images = []
+
+ @classmethod
+ def tearDownClass(cls):
+ for image_id in cls.created_images:
+ cls.client.delete_image(image_id)
+
+ @attr(type='image')
+ def test_register_with_invalid_data(self):
+ """Negative tests for invalid data supplied to POST /images"""
+
+ metas = [
+ {
+ 'id': '1'
+ }, # Cannot specify ID in registration
+ {
+ 'container_format': 'wrong',
+ }, # Invalid container format
+ {
+ 'disk_format': 'wrong',
+ }, # Invalid disk format
+ ]
+ for meta in metas:
+ try:
+ results = self.client.add_image(meta)
+ except exception.Invalid:
+ continue
+ self.fail("Did not raise Invalid for meta %s. Got results: %s" %
+ (meta, results))
+
+ @attr(type='image')
+ def test_register_then_upload(self):
+ """Register, then upload an image"""
+ meta = {
+ 'name': 'New Name',
+ 'is_public': True,
+ 'disk_format': 'vhd',
+ 'container_format': 'bare',
+ 'properties': {'prop1': 'val1'}
+ }
+ results = self.client.add_image(meta)
+ self.assertTrue('id' in results)
+ 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)
+ for key, val in meta['properties'].items():
+ 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'])
+
+ @attr(type='image')
+ @unittest.skip('Skipping until Glance Bug 912897 is fixed')
+ def test_register_remote_image(self):
+ """Register a new remote image"""
+ meta = {
+ 'name': 'New Remote Image',
+ 'is_public': True,
+ 'disk_format': 'raw',
+ 'container_format': 'bare',
+ 'location': 'http://example.com/someimage.iso'
+ }
+ results = self.client.add_image(meta)
+ self.assertTrue('id' in results)
+ 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'])
+
+
+class ListImagesTest(unittest.TestCase):
+
+ """
+ Here we test the listing of image information
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ raise SkipTest('Skipping until Glance Bug 912897 is fixed')
+ cls.os = openstack.ServiceManager()
+ cls.client = cls.os.images.get_client()
+ cls.created_images = []
+ cls.original_images = cls.client.get_images()
+
+ # We add a few images here to test the listing functionality of
+ # the images API
+ for x in xrange(1, 10):
+ # We make even images remote and odd images standard
+ if x % 2 == 0:
+ cls.created_images.append(cls._create_remote_image(x))
+ else:
+ cls.created_images.append(cls._create_standard_image(x))
+
+ @classmethod
+ def tearDownClass(cls):
+ for image_id in cls.created_images:
+ cls.client.delete_image(image_id)
+
+ @classmethod
+ def _create_remote_image(cls, x):
+ """
+ Create a new remote image and return the ID of the newly-registered
+ image
+ """
+ meta = {
+ 'name': 'New Remote Image %s' % x,
+ 'is_public': True,
+ 'disk_format': 'raw',
+ 'container_format': 'bare',
+ 'location': 'http://example.com/someimage_%s.iso' % x
+ }
+ results = cls.client.add_image(meta)
+ image_id = results['id']
+ return image_id
+
+ @classmethod
+ def _create_standard_image(cls, x):
+ """
+ Create a new standard image and return the ID of the newly-registered
+ image. Note that the size of the new image is a random number between
+ 1024 and 4096
+ """
+ meta = {
+ 'name': 'New Standard Image %s' % x,
+ 'is_public': True,
+ 'disk_format': 'raw',
+ 'container_format': 'bare'
+ }
+ image_file = StringIO.StringIO('*' * random.randint(1024, 4096))
+ results = cls.client.add_image(meta, image_file)
+ image_id = results['id']
+ return image_id
+
+ @attr(type='image')
+ def test_index_no_params(self):
+ """
+ Simple test to see all fixture images returned
+ """
+ images = self.client.get_images()
+ self.assertEqual(10, len(images) - len(cls.original_images))
diff --git a/tempest/tools/conf_from_devstack b/tempest/tools/conf_from_devstack
index f3f1309..15346d9 100755
--- a/tempest/tools/conf_from_devstack
+++ b/tempest/tools/conf_from_devstack
@@ -135,8 +135,7 @@
if options.verbose:
print "Found base image with UUID %s" % conf_settings['base_image_uuid']
- tempest_conf = """
-[nova]
+ tempest_conf = """[nova]
host=%(service_host)s
port=%(service_port)s
apiVer=%(identity_api_version)s
@@ -148,6 +147,16 @@
build_interval=10
build_timeout=600
+[image]
+host = %(service_host)s
+port = 9292
+username = %(user)s
+password = %(password)s
+tenant = %(user)s
+auth_url = http://127.0.0.1:5000/v2.0/tokens/
+strategy = keystone
+service_token = servicetoken
+
[environment]
image_ref=%(base_image_uuid)s
image_ref_alt=4