Bootstrapping v2 Image API implementation

* Adding barebones of images, access, schema, and root resources
* Greatly simplify version negotiation middleware
* Partially implements bp api-2

Change-Id: I3ae72354d7ec7f3766aec164d305c52e7315200d
This commit is contained in:
Brian Waldon 2012-03-28 08:33:39 -07:00
parent 6999c60d77
commit 4255bbfb2e
17 changed files with 786 additions and 91 deletions

View File

@ -22,7 +22,6 @@ return
"""
import logging
import re
from glance.api import versions
from glance.common import wsgi
@ -34,91 +33,57 @@ class VersionNegotiationFilter(wsgi.Middleware):
def __init__(self, app, conf, **local_conf):
self.versions_app = versions.Controller(conf)
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
self.conf = conf
super(VersionNegotiationFilter, self).__init__(app)
def process_request(self, req):
"""
If there is a version identifier in the URI, simply
return the correct API controller, otherwise, if we
find an Accept: header, process it
"""
# See if a version identifier is in the URI passed to
# us already. If so, simply return the right version
# API controller
msg = _("Processing request: %(method)s %(path)s Accept: "
"%(accept)s") % ({'method': req.method,
'path': req.path, 'accept': req.accept})
logger.debug(msg)
"""Try to find a version first in the accept header, then the URL"""
msg = _("Determining version of request: %(method)s %(path)s"
" Accept: %(accept)s")
args = {'method': req.method, 'path': req.path, 'accept': req.accept}
logger.debug(msg % args)
# If the request is for /versions, just return the versions container
#TODO(bcwaldon): deprecate this behavior
if req.path_info_peek() == "versions":
return self.versions_app
match = self._match_version_string(req.path_info_peek(), req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug(_("Matched versioned URI. Version: %d.%d"),
req.environ['api.major_version'],
req.environ['api.minor_version'])
# Strip the version from the path
req.path_info_pop()
args = (req.environ['api.major_version'], req.path_info)
req.path_info = '/v%s%s' % args
return None
else:
logger.debug(_("Unknown version in versioned URI: %d.%d. "
"Returning version choices."),
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
accept = str(req.accept)
if accept.startswith('application/vnd.openstack.images-'):
logger.debug(_("Using media-type versioning"))
token_loc = len('application/vnd.openstack.images-')
accept_version = accept[token_loc:]
match = self._match_version_string(accept_version, req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug(_("Matched versioned media type. "
"Version: %d.%d"),
req.environ['api.major_version'],
req.environ['api.minor_version'])
args = (req.environ['api.major_version'], req.path_info)
req.path_info = '/v%s%s' % args
return None
req_version = accept[token_loc:]
else:
logger.debug(_("Unknown version in accept header: %d.%d..."
"returning version choices."),
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
else:
if req.accept not in ('*/*', ''):
logger.debug(_("Unknown accept header: %s..."
"returning version choices."), req.accept)
logger.debug(_("Using url versioning"))
# Remove version in url so it doesn't conflict later
req_version = req.path_info_pop()
try:
version = self._match_version_string(req_version)
except ValueError:
logger.debug(_("Unknown version. Returning version choices."))
return self.versions_app
req.environ['api.version'] = version
req.path_info = ''.join(('/v', str(version), req.path_info))
logger.debug(_("Matched version: v%d"), version)
logger.debug('new uri %s' % req.path_info)
return None
def _match_version_string(self, subject, req):
def _match_version_string(self, subject):
"""
Given a subject string, tries to match a major and/or
minor version number. If found, sets the api.major_version
and api.minor_version environ variables.
Returns True if there was a match, false otherwise.
Given a string, tries to match a major and/or
minor version number.
:param subject: The string to check
:param req: Webob.Request object
:returns version found in the subject
:raises ValueError if no acceptable version could be found
"""
match = self.version_uri_regex.match(subject)
if match:
major_version, minor_version = match.groups(0)
major_version = int(major_version)
minor_version = int(minor_version)
req.environ['api.major_version'] = major_version
req.environ['api.minor_version'] = minor_version
return match is not None
if subject in ('v1', 'v1.0', 'v1.1'):
major_version = 1
elif subject == 'v2':
major_version = 2
else:
raise ValueError()
return major_version

21
glance/api/v2/base.py Normal file
View File

@ -0,0 +1,21 @@
# 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.
class Controller(object):
"""Base API controller class"""
def __init__(self, conf):
self.conf = conf

View File

@ -0,0 +1,58 @@
# 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 webob.exc
import glance.api.v2.base
from glance.common import exception
import glance.registry.db.api
class ImageAccessController(glance.api.v2.base.Controller):
def __init__(self, conf, db=None):
super(ImageAccessController, self).__init__(conf)
self.db_api = db or glance.registry.db.api
self.db_api.configure_db(conf)
def _format_access_record(self, image_member):
return {
'image_id': image_member['image_id'],
'tenant_id': image_member['member'],
'can_share': image_member['can_share'],
'links': self._get_access_record_links(image_member),
}
def _get_access_record_links(self, image_member):
image_id = image_member['image_id']
tenant_id = image_member['member']
self_href = '/v2/images/%s/access/%s' % (image_id, tenant_id)
return [
{'rel': 'self', 'href': self_href},
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
]
def _get_container_links(self, image_id):
return [{'rel': 'self', 'href': '/v2/images/%s/access' % image_id}]
def index(self, req, image_id):
try:
members = self.db_api.get_image_members(req.context, image_id)
except exception.NotFound:
raise webob.exc.HTTPNotFound()
records = [self._format_access_record(m) for m in members]
return {
'access_records': records,
'links': self._get_container_links(image_id),
}

68
glance/api/v2/images.py Normal file
View File

@ -0,0 +1,68 @@
# 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 webob.exc
import glance.api.v2.base
from glance.common import exception
from glance.common import wsgi
import glance.registry.db.api
class ImagesController(glance.api.v2.base.Controller):
"""WSGI controller for images resource in Glance v2 API."""
def __init__(self, conf, db=None):
super(ImagesController, self).__init__(conf)
self.db_api = db or glance.registry.db.api
self.db_api.configure_db(conf)
def _format_image(self, image):
props = ['id', 'name']
items = filter(lambda item: item[0] in props, image.iteritems())
obj = dict(items)
obj['links'] = self._get_image_links(image)
return obj
def _get_image_links(self, image):
image_id = image['id']
return [
{'rel': 'self', 'href': '/v2/images/%s' % image_id},
{'rel': 'access', 'href': '/v2/images/%s/access' % image_id},
{'rel': 'describedby', 'href': '/v2/schemas/image'},
]
def _get_container_links(self, images):
return []
def index(self, req):
images = self.db_api.image_get_all(req.context)
return {
'images': [self._format_image(i) for i in images],
'links': self._get_container_links(images),
}
def show(self, req, id):
try:
image = self.db_api.image_get(req.context, id)
except exception.ImageNotFound:
raise webob.exc.HTTPNotFound()
return self._format_image(image)
def create_resource(conf):
"""Images resource factory method"""
controller = ImagesController(conf)
return wsgi.Resource(controller)

34
glance/api/v2/root.py Normal file
View File

@ -0,0 +1,34 @@
# 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 glance.common import wsgi
class RootController(object):
"""WSGI controller for root resource (/) in Glance v2 API."""
def index(self, req):
return {
'links': [
{'rel': 'schemas', 'href': '/v2/schemas'},
{'rel': 'images', 'href': '/v2/images'},
],
}
def create_resource(conf):
"""Root resource factory method"""
controller = RootController()
return wsgi.Resource(controller)

41
glance/api/v2/router.py Normal file
View File

@ -0,0 +1,41 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 routes
#from glance.api.v1 import images
#from glance.api.v1 import members
from glance.api.v2 import root
from glance.common import wsgi
logger = logging.getLogger(__name__)
class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, conf, **local_conf):
self.conf = conf
mapper = routes.Mapper()
root_resource = root.create_resource(conf)
mapper.connect('/', controller=root_resource, action='index')
super(API, self).__init__(mapper)

74
glance/api/v2/schemas.py Normal file
View File

@ -0,0 +1,74 @@
# 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 glance.api.v2.base
from glance.common import wsgi
class SchemasController(glance.api.v2.base.Controller):
def index(self, req):
links = [
{'rel': 'image', 'href': '/schemas/image'},
{'rel': 'access', 'href': '/schemas/image/access'},
]
return {'links': links}
def image(self, req):
return {
"name": "image",
"properties": {
"id": {
"type": "string",
"description": "An identifier for the image",
"required": True,
"maxLength": 32,
"readonly": True
},
"name": {
"type": "string",
"description": "Descriptive name for the image",
"required": True,
},
},
}
def access(self, req):
return {
'name': 'access',
'properties': {
"image_id": {
"type": "string",
"description": "The image identifier",
"required": True,
"maxLength": 32,
},
"tenant_id": {
"type": "string",
"description": "The tenant identifier",
"required": True,
},
"can_share": {
"type": "boolean",
"description": "Ability of tenant to share with others",
"required": True,
"default": False,
},
},
}
def create_resource(conf):
controller = SchemasController(conf)
return wsgi.Resource(controller)

View File

@ -72,6 +72,10 @@ class NotFound(GlanceException):
message = _("An object with the specified identifier was not found.")
class ImageNotFound(NotFound):
message = _("Image %(image_id)s was not found.")
class UnknownScheme(GlanceException):
message = _("Unknown scheme '%(scheme)s' found in URI")

View File

@ -267,6 +267,7 @@ pipeline = versionnegotiation fakeauth context rootapp
use = egg:Paste#urlmap
/: apiversions
/v1: apiv1app
/v2: apiv2app
[app:apiversions]
paste.app_factory = glance.api.versions:create_resource
@ -275,6 +276,10 @@ paste.app_factory = glance.api.versions:create_resource
paste.app_factory = glance.common.wsgi:app_factory
glance.app_factory = glance.api.v1.router:API
[app:apiv2app]
paste.app_factory = glance.common.wsgi:app_factory
glance.app_factory = glance.api.v2.router:API
[filter:versionnegotiation]
paste.filter_factory = glance.common.wsgi:filter_factory
glance.filter_factory =

View File

@ -87,8 +87,6 @@ class TestRootApi(functional.FunctionalTest):
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 4. GET / with an Accept: application/vnd.openstack.images-v1
# Verify empty image list returned
@ -108,24 +106,20 @@ class TestRootApi(functional.FunctionalTest):
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 6. GET /v1.0/images with no Accept: header
# Verify empty image list returned
path = 'http://%s:%d/v1.0/images' % ('0.0.0.0', self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
# 7. GET /v1.a/images with no Accept: header
# Verify empty image list returned
# Verify version choices returned
path = 'http://%s:%d/v1.a/images' % ('0.0.0.0', self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
self.assertEqual(response.status, 300)
# 7. GET /v1.a/images with no Accept: header
# Verify version choices returned
path = 'http://%s:%d/v1.a/images' % ('0.0.0.0', self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
# 8. GET /va.1/images with no Accept: header
# Verify version choices returned
@ -159,9 +153,8 @@ class TestRootApi(functional.FunctionalTest):
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 404)
# 12. GET /v2/versions with no Accept: header
# Verify version choices returned
path = 'http://%s:%d/v2/versions' % ('0.0.0.0', self.api_port)
path = 'http://%s:%d/v10' % ('0.0.0.0', self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
@ -172,12 +165,10 @@ class TestRootApi(functional.FunctionalTest):
# about unknown version in accept header.
path = 'http://%s:%d/images' % ('0.0.0.0', self.api_port)
http = httplib2.Http()
headers = {'Accept': 'application/vnd.openstack.images-v2'}
headers = {'Accept': 'application/vnd.openstack.images-v10'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 14. GET /v1.2/images with no Accept: header
# Verify version choices returned
@ -186,7 +177,5 @@ class TestRootApi(functional.FunctionalTest):
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown version in versioned URI'
in open(self.api_server.log_file).read())
self.stop_servers()

View File

@ -0,0 +1,39 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 requests
from glance.tests import functional
class TestRoot(functional.FunctionalTest):
def test_root_request(self):
self.cleanup()
self.start_servers(**self.__dict__.copy())
path = "http://%s:%d/v2/" % ("0.0.0.0", self.api_port)
response = requests.get(path)
self.assertEqual(response.status_code, 200)
expected = {
'links': [
{'href': "/v2/schemas", "rel": "schemas"},
{"href": "/v2/images", "rel": "images"},
],
}
self.assertEqual(response.text, json.dumps(expected))

View File

@ -0,0 +1,79 @@
# 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 glance.common import exception
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
UUID2 = '971ec09a-8067-4bc8-a91f-ae3557f1c4c7'
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
class FakeRequest(object):
@property
def context(self):
return
class FakeDB(object):
def __init__(self):
self.images = {
UUID1: self._image_format(UUID1),
UUID2: self._image_format(UUID2),
}
self.members = {
UUID1: [
self._image_member_format(UUID1, TENANT1, True),
self._image_member_format(UUID1, TENANT2, False),
],
UUID2: [],
}
def reset(self):
self.images = {}
self.members = {}
def configure_db(*args, **kwargs):
pass
def _image_member_format(self, image_id, tenant_id, can_share):
return {
'image_id': image_id,
'member': tenant_id,
'can_share': can_share,
}
def _image_format(self, image_id):
return {'id': image_id, 'name': 'image-name', 'foo': 'bar'}
def image_get(self, context, image_id):
try:
return self.images[image_id]
except KeyError:
raise exception.ImageNotFound(image_id=image_id)
def image_get_all(self, context):
return self.images.values()
def get_image_members(self, context, image_id):
try:
self.images[image_id]
except KeyError:
raise exception.ImageNotFound()
else:
return self.members.get(image_id, [])

View File

@ -0,0 +1,99 @@
# 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 webob.exc
import glance.api.v2.image_access
from glance.common import utils
import glance.tests.unit.utils as test_utils
class TestImageAccessController(unittest.TestCase):
def setUp(self):
super(TestImageAccessController, self).setUp()
self.db = test_utils.FakeDB()
self.controller = \
glance.api.v2.image_access.ImageAccessController({}, self.db)
def test_index(self):
req = test_utils.FakeRequest()
output = self.controller.index(req, test_utils.UUID1)
expected = {
'access_records': [
{
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'can_share': True,
'links': [
{
'rel': 'self',
'href': ('/v2/images/%s/access/%s' %
(test_utils.UUID1, test_utils.TENANT1)),
},
{
'rel': 'describedby',
'href': '/v2/schemas/image/access',
},
],
},
{
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT2,
'can_share': False,
'links': [
{
'rel': 'self',
'href': ('/v2/images/%s/access/%s' %
(test_utils.UUID1, test_utils.TENANT2)),
},
{
'rel': 'describedby',
'href': '/v2/schemas/image/access',
},
],
},
],
'links': [
{
'rel': 'self',
'href': '/v2/images/%s/access' % test_utils.UUID1,
},
],
}
self.assertEqual(expected, output)
def test_index_zero_records(self):
req = test_utils.FakeRequest()
output = self.controller.index(req, test_utils.UUID2)
expected = {
'access_records': [],
'links': [
{
'rel': 'self',
'href': '/v2/images/%s/access' % test_utils.UUID2,
},
],
}
self.assertEqual(expected, output)
def test_index_nonexistant_image(self):
req = test_utils.FakeRequest()
image_id = utils.generate_uuid()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.index, req, image_id)

View File

@ -0,0 +1,97 @@
# 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 webob
import glance.api.v2.images
from glance.common import utils
import glance.tests.unit.utils as test_utils
class TestImagesController(unittest.TestCase):
def setUp(self):
super(TestImagesController, self).setUp()
self.db = test_utils.FakeDB()
self.controller = glance.api.v2.images.ImagesController({}, self.db)
def test_index(self):
req = test_utils.FakeRequest()
output = self.controller.index(req)
expected = {
'images': [
{
'id': test_utils.UUID1,
'name': 'image-name',
'links': [
{
'rel': 'self',
'href': '/v2/images/%s' % test_utils.UUID1,
},
{
'rel': 'access',
'href': '/v2/images/%s/access' % test_utils.UUID1,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
{
'id': test_utils.UUID2,
'name': 'image-name',
'links': [
{
'rel': 'self',
'href': '/v2/images/%s' % test_utils.UUID2,
},
{
'rel': 'access',
'href': '/v2/images/%s/access' % test_utils.UUID2,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
},
],
'links': [],
}
self.assertEqual(expected, output)
def test_index_zero_images(self):
self.db.reset()
req = test_utils.FakeRequest()
output = self.controller.index(req)
self.assertEqual({'images': [], 'links': []}, output)
def test_show(self):
req = test_utils.FakeRequest()
output = self.controller.show(req, id=test_utils.UUID2)
expected = {
'id': test_utils.UUID2,
'name': 'image-name',
'links': [
{'rel': 'self', 'href': '/v2/images/%s' % test_utils.UUID2},
{
'rel': 'access',
'href': '/v2/images/%s/access' % test_utils.UUID2,
},
{'rel': 'describedby', 'href': '/v2/schemas/image'}
],
}
self.assertEqual(expected, output)
def test_show_non_existant(self):
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
test_utils.FakeRequest(), id=utils.generate_uuid())

View File

@ -0,0 +1,37 @@
# 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 glance.api.v2.root
import glance.tests.unit.utils as test_utils
class TestRootController(unittest.TestCase):
def setUp(self):
super(TestRootController, self).setUp()
self.controller = glance.api.v2.root.RootController()
def test_index(self):
req = test_utils.FakeRequest()
output = self.controller.index(req)
expected = {
'links': [
{'rel': 'schemas', 'href': '/v2/schemas'},
{'rel': 'images', 'href': '/v2/images'},
],
}
self.assertEqual(expected, output)

View File

@ -0,0 +1,84 @@
# 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 glance.api.v2.schemas
import glance.tests.unit.utils as test_utils
class TestSchemasController(unittest.TestCase):
def setUp(self):
super(TestSchemasController, self).setUp()
self.controller = glance.api.v2.schemas.SchemasController({})
def test_index(self):
req = test_utils.FakeRequest()
output = self.controller.index(req)
expected = {'links': [
{'rel': 'image', 'href': '/schemas/image'},
{'rel': 'access', 'href': '/schemas/image/access'},
]}
self.assertEqual(expected, output)
def test_image(self):
req = test_utils.FakeRequest()
output = self.controller.image(req)
expected = {
'name': 'image',
'properties': {
'id': {
'type': 'string',
'description': 'An identifier for the image',
'required': True,
'maxLength': 32,
'readonly': True
},
'name': {
'type': 'string',
'description': 'Descriptive name for the image',
'required': True,
},
},
}
self.assertEqual(expected, output)
def test_access(self):
req = test_utils.FakeRequest()
output = self.controller.access(req)
expected = {
'name': 'access',
'properties': {
"image_id": {
"type": "string",
"description": "The image identifier",
"required": True,
"maxLength": 32,
},
"tenant_id": {
"type": "string",
"description": "The tenant identifier",
"required": True,
},
"can_share": {
"type": "boolean",
"description": "Ability of tenant to share with others",
"required": True,
"default": False,
},
},
}
self.assertEqual(output, expected)

View File

@ -12,3 +12,4 @@ nosexcover
openstack.nose_plugin
pep8==0.6.1
sphinx>=1.1.2
requests