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:
parent
6999c60d77
commit
4255bbfb2e
@ -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
21
glance/api/v2/base.py
Normal 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
|
58
glance/api/v2/image_access.py
Normal file
58
glance/api/v2/image_access.py
Normal 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
68
glance/api/v2/images.py
Normal 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
34
glance/api/v2/root.py
Normal 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
41
glance/api/v2/router.py
Normal 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
74
glance/api/v2/schemas.py
Normal 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)
|
@ -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")
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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()
|
||||
|
39
glance/tests/functional/v2/test_root.py
Normal file
39
glance/tests/functional/v2/test_root.py
Normal 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))
|
79
glance/tests/unit/utils.py
Normal file
79
glance/tests/unit/utils.py
Normal 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, [])
|
99
glance/tests/unit/v2/test_image_access_resource.py
Normal file
99
glance/tests/unit/v2/test_image_access_resource.py
Normal 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)
|
97
glance/tests/unit/v2/test_images_resource.py
Normal file
97
glance/tests/unit/v2/test_images_resource.py
Normal 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())
|
37
glance/tests/unit/v2/test_root_resource.py
Normal file
37
glance/tests/unit/v2/test_root_resource.py
Normal 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)
|
84
glance/tests/unit/v2/test_schemas_resource.py
Normal file
84
glance/tests/unit/v2/test_schemas_resource.py
Normal 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)
|
@ -12,3 +12,4 @@ nosexcover
|
||||
openstack.nose_plugin
|
||||
pep8==0.6.1
|
||||
sphinx>=1.1.2
|
||||
requests
|
||||
|
Loading…
Reference in New Issue
Block a user