API v2 controller/serialization separation

* images and access are now separated into deserializers, controllers, and serializers
* now using schemas to valide incoming requests
* adding create methods for image access
* removing ImageNotFound as it was only being generated by fakes, using NotFound until the backend is updated

Change-Id: Ida3c1b117ee0147e818b74518e84ef9101cbdfc3
This commit is contained in:
Brian Waldon 2012-04-10 21:53:28 -07:00
parent fc816ee029
commit 5182c1eb21
9 changed files with 463 additions and 232 deletions

View File

@ -13,46 +13,84 @@
# License for the specific language governing permissions and limitations
# under the License.
import webob.exc
import json
import glance.api.v2.base
from glance.common import exception
import jsonschema
from glance.api.v2 import base
from glance.api.v2 import schemas
from glance.common import wsgi
import glance.registry.db.api
class ImageAccessController(glance.api.v2.base.Controller):
class ImageAccessController(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 index(self, req, image_id):
image = self.db_api.image_get(req.context, image_id)
return image['members']
def _get_access_record_links(self, image_member):
def show(self, req, image_id, tenant_id):
return self.db_api.image_member_find(req.context, image_id, tenant_id)
def create(self, req, access):
return self.db_api.image_member_create(req.context, access)
class RequestDeserializer(wsgi.JSONRequestDeserializer):
def __init__(self, conf):
super(RequestDeserializer, self).__init__()
self.conf = conf
def _validate(self, request, obj):
schema = schemas.SchemasController(self.conf).access(request)
jsonschema.validate(obj, schema)
def create(self, request):
output = super(RequestDeserializer, self).default(request)
body = output.pop('body')
self._validate(request, body)
body['member'] = body.pop('tenant_id')
output['access'] = body
return output
class ResponseSerializer(wsgi.JSONResponseSerializer):
def _get_access_href(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 '/v2/images/%s/access/%s' % (image_id, tenant_id)
def _get_access_links(self, access):
return [
{'rel': 'self', 'href': self_href},
{'rel': 'self', 'href': self._get_access_href(access)},
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
]
def _format_access(self, access):
return {
'image_id': access['image_id'],
'tenant_id': access['member'],
'can_share': access['can_share'],
'links': self._get_access_links(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),
def show(self, response, access):
response.body = json.dumps({'access': self._format_access(access)})
def index(self, response, access_records):
body = {
'access_records': [self._format_access(a) for a in access_records],
'links': [],
}
response.body = json.dumps(body)
def create(self, response, access):
response.body = json.dumps({'access': self._format_access(access)})
response.location = self._get_access_href(access)

View File

@ -13,22 +13,56 @@
# License for the specific language governing permissions and limitations
# under the License.
import webob.exc
import json
import glance.api.v2.base
from glance.common import exception
import jsonschema
from glance.api.v2 import base
from glance.api.v2 import schemas
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."""
class ImagesController(base.Controller):
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 index(self, req):
return self.db_api.image_get_all(req.context)
def show(self, req, id):
return self.db_api.image_get(req.context, id)
class RequestDeserializer(wsgi.JSONRequestDeserializer):
def __init__(self, conf):
super(RequestDeserializer, self).__init__()
self.conf = conf
def _validate(self, request, obj):
schema = schemas.SchemasController(self.conf).image(request)
jsonschema.validate(obj, schema)
def create(self, request):
output = super(RequestDeserializer, self).default(request)
body = output.pop('body')
self._validate(request, body)
output['image'] = body
return output
class ResponseSerializer(wsgi.JSONResponseSerializer):
def _get_image_href(self, image):
return '/v2/images/%s' % image['id']
def _get_image_links(self, image):
return [
{'rel': 'self', 'href': self._get_image_href(image)},
{'rel': 'describedby', 'href': '/v2/schemas/image'},
]
def _format_image(self, image):
props = ['id', 'name']
items = filter(lambda item: item[0] in props, image.iteritems())
@ -36,30 +70,19 @@ class ImagesController(glance.api.v2.base.Controller):
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 create(self, response, image):
response.body = json.dumps({'image': self._format_image(image)})
response.location = self._get_image_href(image)
def _get_container_links(self, images):
return []
def show(self, response, image):
response.body = json.dumps({'image': self._format_image(image)})
def index(self, req):
images = self.db_api.image_get_all(req.context)
return {
def index(self, response, images):
body = {
'images': [self._format_image(i) for i in images],
'links': self._get_container_links(images),
'links': [],
}
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)
response.body = json.dumps(body)
def create_resource(conf):

View File

@ -17,24 +17,15 @@ 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 {
#NOTE(bcwaldon): this is temporary until we generate them on the fly
IMAGE_SCHEMA = {
"name": "image",
"properties": {
"id": {
"type": "string",
"description": "An identifier for the image",
"required": True,
"maxLength": 32,
"readonly": True
"required": False,
"maxLength": 36,
},
"name": {
"type": "string",
@ -44,15 +35,14 @@ class SchemasController(glance.api.v2.base.Controller):
},
}
def access(self, req):
return {
ACCESS_SCHEMA = {
'name': 'access',
'properties': {
"image_id": {
"type": "string",
"description": "The image identifier",
"required": True,
"maxLength": 32,
"maxLength": 36,
},
"tenant_id": {
"type": "string",
@ -69,6 +59,21 @@ class SchemasController(glance.api.v2.base.Controller):
}
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 IMAGE_SCHEMA
def access(self, req):
return ACCESS_SCHEMA
def create_resource(conf):
controller = SchemasController(conf)
return wsgi.Resource(controller)

View File

@ -72,10 +72,6 @@ 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

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import webob
from glance.common import exception
@ -23,7 +25,11 @@ TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
class FakeRequest(object):
class FakeRequest(webob.Request):
def __init__(self):
#TODO(bcwaldon): figure out how to fake this out cleanly
super(FakeRequest, self).__init__({'REQUEST_METHOD': 'POST'})
@property
def context(self):
return
@ -44,6 +50,9 @@ class FakeDB(object):
UUID2: [],
}
self.images[UUID1]['members'] = self.members[UUID1]
self.images[UUID2]['members'] = self.members[UUID2]
def reset(self):
self.images = {}
self.members = {}
@ -59,21 +68,38 @@ class FakeDB(object):
}
def _image_format(self, image_id):
return {'id': image_id, 'name': 'image-name', 'foo': 'bar'}
return {'id': image_id, 'name': 'image-name'}
def image_get(self, context, image_id):
try:
return self.images[image_id]
image = self.images[image_id]
except KeyError:
raise exception.ImageNotFound(image_id=image_id)
raise exception.NotFound(image_id=image_id)
#NOTE(bcwaldon: this is a hack until we can get image members with
# a direct db call
image['members'] = self.members.get(image_id, [])
return image
def image_get_all(self, context):
return self.images.values()
def get_image_members(self, context, image_id):
def image_member_find(self, context, image_id, tenant_id):
try:
self.images[image_id]
except KeyError:
raise exception.ImageNotFound()
else:
return self.members.get(image_id, [])
raise exception.NotFound()
for member in self.members.get(image_id, []):
if member['member'] == tenant_id:
return member
raise exception.NotFound()
def image_member_create(self, context, values):
member = self._image_member_format(values['image_id'],
values['member'],
values['can_share'])
self.members[values['image_id']] = member
return member

View File

@ -13,11 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import unittest
import webob.exc
import jsonschema
import webob
import glance.api.v2.image_access
from glance.common import exception
from glance.common import utils
import glance.tests.unit.utils as test_utils
@ -33,67 +36,200 @@ class TestImageAccessController(unittest.TestCase):
def test_index(self):
req = test_utils.FakeRequest()
output = self.controller.index(req, test_utils.UUID1)
expected = {
'access_records': [
expected = [
{
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'member': 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,
'member': 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,
},
],
}
expected = []
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.assertRaises(exception.NotFound,
self.controller.index, req, image_id)
def test_show(self):
req = test_utils.FakeRequest()
image_id = test_utils.UUID1
tenant_id = test_utils.TENANT1
output = self.controller.show(req, image_id, tenant_id)
expected = {
'image_id': image_id,
'member': tenant_id,
'can_share': True,
}
self.assertEqual(expected, output)
def test_show_nonexistant_image(self):
req = test_utils.FakeRequest()
image_id = utils.generate_uuid()
tenant_id = test_utils.TENANT1
self.assertRaises(exception.NotFound,
self.controller.show, req, image_id, tenant_id)
def test_show_nonexistant_tenant(self):
req = test_utils.FakeRequest()
image_id = test_utils.UUID1
tenant_id = utils.generate_uuid()
self.assertRaises(exception.NotFound,
self.controller.show, req, image_id, tenant_id)
def test_create(self):
fixture = {
'image_id': test_utils.UUID1,
'member': utils.generate_uuid(),
'can_share': True,
}
req = test_utils.FakeRequest()
output = self.controller.create(req, fixture)
self.assertEqual(fixture, output)
class TestImageAccessDeserializer(unittest.TestCase):
def setUp(self):
self.deserializer = glance.api.v2.image_access.RequestDeserializer({})
def test_create(self):
fixture = {
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'can_share': False,
}
expected = {
'image_id': test_utils.UUID1,
'member': test_utils.TENANT1,
'can_share': False,
}
request = test_utils.FakeRequest()
request.body = json.dumps(fixture)
output = self.deserializer.create(request)
self.assertEqual(output, {'access': expected})
def _test_create_fails(self, fixture):
request = test_utils.FakeRequest()
request.body = json.dumps(fixture)
self.assertRaises(jsonschema.ValidationError,
self.deserializer.create, request)
def test_create_no_image(self):
fixture = {'tenant_id': test_utils.TENANT1, 'can_share': True}
self._test_create_fails(fixture)
class TestImageAccessSerializer(unittest.TestCase):
serializer = glance.api.v2.image_access.ResponseSerializer()
def test_show(self):
fixture = {
'member': test_utils.TENANT1,
'image_id': test_utils.UUID1,
'can_share': False,
}
self_href = ('/v2/images/%s/access/%s' %
(test_utils.UUID1, test_utils.TENANT1))
expected = {
'access': {
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'can_share': False,
'links': [
{'rel': 'self', 'href': self_href},
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
],
},
}
response = webob.Response()
self.serializer.show(response, fixture)
self.assertEqual(expected, json.loads(response.body))
def test_index(self):
fixtures = [
{
'member': test_utils.TENANT1,
'image_id': test_utils.UUID1,
'can_share': False,
},
{
'member': test_utils.TENANT2,
'image_id': test_utils.UUID2,
'can_share': True,
},
]
expected = {
'access_records': [
{
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'can_share': False,
'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.UUID2,
'tenant_id': test_utils.TENANT2,
'can_share': True,
'links': [
{
'rel': 'self',
'href': ('/v2/images/%s/access/%s' %
(test_utils.UUID2, test_utils.TENANT2))
},
{
'rel': 'describedby',
'href': '/v2/schemas/image/access',
},
],
},
],
'links': [],
}
response = webob.Response()
self.serializer.index(response, fixtures)
self.assertEqual(expected, json.loads(response.body))
def test_create(self):
fixture = {
'member': test_utils.TENANT1,
'image_id': test_utils.UUID1,
'can_share': False,
}
self_href = ('/v2/images/%s/access/%s' %
(test_utils.UUID1, test_utils.TENANT1))
expected = {
'access': {
'image_id': test_utils.UUID1,
'tenant_id': test_utils.TENANT1,
'can_share': False,
'links': [
{'rel': 'self', 'href': self_href},
{'rel': 'describedby', 'href': '/v2/schemas/image/access'},
],
},
}
response = webob.Response()
self.serializer.create(response, fixture)
self.assertEqual(expected, json.loads(response.body))
self.assertEqual(self_href, response.location)

View File

@ -13,85 +13,130 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import unittest
import jsonschema
import webob
import glance.api.v2.images
from glance.common import exception
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)
request = test_utils.FakeRequest()
output = self.controller.index(request)
self.assertEqual(2, len(output))
self.assertEqual(output[0]['id'], test_utils.UUID1)
self.assertEqual(output[1]['id'], test_utils.UUID2)
def test_index_zero_images(self):
self.db.reset()
request = test_utils.FakeRequest()
output = self.controller.index(request)
self.assertEqual([], output)
def test_show(self):
request = test_utils.FakeRequest()
output = self.controller.show(request, id=test_utils.UUID2)
self.assertEqual(output['id'], test_utils.UUID2)
def test_show_non_existant(self):
self.assertRaises(exception.NotFound, self.controller.show,
test_utils.FakeRequest(), id=utils.generate_uuid())
class TestImagesDeserializer(unittest.TestCase):
def setUp(self):
self.deserializer = glance.api.v2.images.RequestDeserializer({})
def test_create(self):
request = test_utils.FakeRequest()
request.body = json.dumps({'name': 'image-1'})
output = self.deserializer.create(request)
self.assertEqual(output, {'image': {'name': 'image-1'}})
def test_create_with_id(self):
request = test_utils.FakeRequest()
image_id = utils.generate_uuid()
request.body = json.dumps({'id': image_id, 'name': 'image-1'})
output = self.deserializer.create(request)
self.assertEqual(output,
{'image': {'id': image_id, 'name': 'image-1'}})
def _test_create_fails(self, body):
request = test_utils.FakeRequest()
request.body = json.dumps(body)
self.assertRaises(jsonschema.ValidationError,
self.deserializer.create, request)
def test_create_no_name(self):
self._test_create_fails({})
class TestImagesSerializer(unittest.TestCase):
def setUp(self):
self.serializer = glance.api.v2.images.ResponseSerializer()
def test_index(self):
fixtures = [
{'id': test_utils.UUID1, 'name': 'image-1'},
{'id': test_utils.UUID2, 'name': 'image-2'},
]
expected = {
'images': [
{
'id': test_utils.UUID1,
'name': 'image-name',
'name': 'image-1',
'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',
'name': 'image-2',
'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)
response = webob.Response()
self.serializer.index(response, fixtures)
self.assertEqual(expected, json.loads(response.body))
def test_show(self):
req = test_utils.FakeRequest()
output = self.controller.show(req, id=test_utils.UUID2)
fixture = {'id': test_utils.UUID2, 'name': 'image-2'}
expected = {
'image': {
'id': test_utils.UUID2,
'name': 'image-name',
'name': 'image-2',
'links': [
{'rel': 'self', 'href': '/v2/images/%s' % test_utils.UUID2},
{
'rel': 'access',
'href': '/v2/images/%s/access' % test_utils.UUID2,
'rel': 'self',
'href': '/v2/images/%s' % 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())
response = webob.Response()
self.serializer.show(response, fixture)
self.assertEqual(expected, json.loads(response.body))

View File

@ -37,48 +37,9 @@ class TestSchemasController(unittest.TestCase):
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)
self.assertEqual(glance.api.v2.schemas.IMAGE_SCHEMA, 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)
self.assertEqual(glance.api.v2.schemas.ACCESS_SCHEMA, output)

View File

@ -39,3 +39,4 @@ lxml
Paste
passlib
jsonschema