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
This commit is contained in:
Jay Pipes 2012-01-06 15:39:20 -05:00
parent d3bb405282
commit 5067728a02
8 changed files with 386 additions and 2 deletions

View File

@ -136,6 +136,62 @@ class EnvironmentConfig(object):
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 @@ class TempestConfig(object):
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."""

View File

@ -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 @@ class Manager(object):
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']

View File

@ -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

View File

View File

@ -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

View File

View File

@ -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))

View File

@ -135,8 +135,7 @@ def main():
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 @@ ssh_timeout=300
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