From 4255bbfb2ee80abdca9c9858a7076bf4b52180be Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 28 Mar 2012 08:33:39 -0700 Subject: [PATCH] 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 --- glance/api/middleware/version_negotiation.py | 103 ++++++------------ glance/api/v2/base.py | 21 ++++ glance/api/v2/image_access.py | 58 ++++++++++ glance/api/v2/images.py | 68 ++++++++++++ glance/api/v2/root.py | 34 ++++++ glance/api/v2/router.py | 41 +++++++ glance/api/v2/schemas.py | 74 +++++++++++++ glance/common/exception.py | 4 + glance/tests/functional/__init__.py | 5 + glance/tests/functional/test_api.py | 33 ++---- glance/tests/functional/v2/test_root.py | 39 +++++++ glance/tests/unit/utils.py | 79 ++++++++++++++ .../unit/v2/test_image_access_resource.py | 99 +++++++++++++++++ glance/tests/unit/v2/test_images_resource.py | 97 +++++++++++++++++ glance/tests/unit/v2/test_root_resource.py | 37 +++++++ glance/tests/unit/v2/test_schemas_resource.py | 84 ++++++++++++++ tools/test-requires | 1 + 17 files changed, 786 insertions(+), 91 deletions(-) create mode 100644 glance/api/v2/base.py create mode 100644 glance/api/v2/image_access.py create mode 100644 glance/api/v2/images.py create mode 100644 glance/api/v2/root.py create mode 100644 glance/api/v2/router.py create mode 100644 glance/api/v2/schemas.py create mode 100644 glance/tests/functional/v2/test_root.py create mode 100644 glance/tests/unit/utils.py create mode 100644 glance/tests/unit/v2/test_image_access_resource.py create mode 100644 glance/tests/unit/v2/test_images_resource.py create mode 100644 glance/tests/unit/v2/test_root_resource.py create mode 100644 glance/tests/unit/v2/test_schemas_resource.py diff --git a/glance/api/middleware/version_negotiation.py b/glance/api/middleware/version_negotiation.py index 35c2dbc79c..9bf6357142 100644 --- a/glance/api/middleware/version_negotiation.py +++ b/glance/api/middleware/version_negotiation.py @@ -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 - 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 + req_version = accept[token_loc:] 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 diff --git a/glance/api/v2/base.py b/glance/api/v2/base.py new file mode 100644 index 0000000000..a4a79ed421 --- /dev/null +++ b/glance/api/v2/base.py @@ -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 diff --git a/glance/api/v2/image_access.py b/glance/api/v2/image_access.py new file mode 100644 index 0000000000..5ef4627638 --- /dev/null +++ b/glance/api/v2/image_access.py @@ -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), + } diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py new file mode 100644 index 0000000000..a301001199 --- /dev/null +++ b/glance/api/v2/images.py @@ -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) diff --git a/glance/api/v2/root.py b/glance/api/v2/root.py new file mode 100644 index 0000000000..773814a1c6 --- /dev/null +++ b/glance/api/v2/root.py @@ -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) diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py new file mode 100644 index 0000000000..bd2a8067bd --- /dev/null +++ b/glance/api/v2/router.py @@ -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) diff --git a/glance/api/v2/schemas.py b/glance/api/v2/schemas.py new file mode 100644 index 0000000000..e8859a03de --- /dev/null +++ b/glance/api/v2/schemas.py @@ -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) diff --git a/glance/common/exception.py b/glance/common/exception.py index 980158f32e..907dde4d5d 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -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") diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index 3366e0f55e..b4a0514a30 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -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 = diff --git a/glance/tests/functional/test_api.py b/glance/tests/functional/test_api.py index d6ecb2ad65..c7b5c3031d 100644 --- a/glance/tests/functional/test_api.py +++ b/glance/tests/functional/test_api.py @@ -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() diff --git a/glance/tests/functional/v2/test_root.py b/glance/tests/functional/v2/test_root.py new file mode 100644 index 0000000000..1861dd559b --- /dev/null +++ b/glance/tests/functional/v2/test_root.py @@ -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)) diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py new file mode 100644 index 0000000000..97ee9007ba --- /dev/null +++ b/glance/tests/unit/utils.py @@ -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, []) diff --git a/glance/tests/unit/v2/test_image_access_resource.py b/glance/tests/unit/v2/test_image_access_resource.py new file mode 100644 index 0000000000..707ba4a1cb --- /dev/null +++ b/glance/tests/unit/v2/test_image_access_resource.py @@ -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) diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py new file mode 100644 index 0000000000..fe9866c7c3 --- /dev/null +++ b/glance/tests/unit/v2/test_images_resource.py @@ -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()) diff --git a/glance/tests/unit/v2/test_root_resource.py b/glance/tests/unit/v2/test_root_resource.py new file mode 100644 index 0000000000..36d8944a76 --- /dev/null +++ b/glance/tests/unit/v2/test_root_resource.py @@ -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) diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py new file mode 100644 index 0000000000..dba02f9565 --- /dev/null +++ b/glance/tests/unit/v2/test_schemas_resource.py @@ -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) diff --git a/tools/test-requires b/tools/test-requires index 0bb67f4aad..5ef9a943fc 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -12,3 +12,4 @@ nosexcover openstack.nose_plugin pep8==0.6.1 sphinx>=1.1.2 +requests