Move the particulars of v2 schemas under v2
Implements blueprint api-v2-refactor-schemas Change-Id: I6ddb0aa9a8fdac2d9a7210e0d0b0fb91e8220396
This commit is contained in:
parent
5a45f02152
commit
5599c951e0
@ -21,6 +21,7 @@ from glance.common import exception
|
||||
from glance.common import utils
|
||||
from glance.common import wsgi
|
||||
import glance.db
|
||||
import glance.schema
|
||||
|
||||
|
||||
class Controller(object):
|
||||
@ -76,14 +77,14 @@ class Controller(object):
|
||||
|
||||
|
||||
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||
def __init__(self, schema_api):
|
||||
def __init__(self):
|
||||
super(RequestDeserializer, self).__init__()
|
||||
self.schema_api = schema_api
|
||||
self.schema = get_schema()
|
||||
|
||||
def create(self, request):
|
||||
output = super(RequestDeserializer, self).default(request)
|
||||
body = output.pop('body')
|
||||
self.schema_api.validate('access', body)
|
||||
self.schema.validate(body)
|
||||
body['member'] = body.pop('tenant_id')
|
||||
output['access_record'] = body
|
||||
return output
|
||||
@ -135,9 +136,24 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
response.status_int = 204
|
||||
|
||||
|
||||
def create_resource(schema_api):
|
||||
def get_schema():
|
||||
properties = {
|
||||
'tenant_id': {
|
||||
'type': 'string',
|
||||
'description': 'The tenant identifier',
|
||||
},
|
||||
'can_share': {
|
||||
'type': 'boolean',
|
||||
'description': 'Ability of tenant to share with others',
|
||||
'default': False,
|
||||
},
|
||||
}
|
||||
return glance.schema.Schema('access', properties)
|
||||
|
||||
|
||||
def create_resource():
|
||||
"""Image access resource factory method"""
|
||||
deserializer = RequestDeserializer(schema_api)
|
||||
deserializer = RequestDeserializer()
|
||||
serializer = ResponseSerializer()
|
||||
controller = Controller()
|
||||
return wsgi.Resource(controller, deserializer, serializer)
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
import webob.exc
|
||||
|
||||
@ -24,8 +25,11 @@ from glance.common import wsgi
|
||||
import glance.db
|
||||
from glance.openstack.common import cfg
|
||||
from glance.openstack.common import timeutils
|
||||
import glance.schema
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@ -136,14 +140,14 @@ class ImagesController(object):
|
||||
|
||||
|
||||
class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||
def __init__(self, schema_api):
|
||||
def __init__(self, schema=None):
|
||||
super(RequestDeserializer, self).__init__()
|
||||
self.schema_api = schema_api
|
||||
self.schema = schema or get_schema()
|
||||
|
||||
def _parse_image(self, request):
|
||||
output = super(RequestDeserializer, self).default(request)
|
||||
body = output.pop('body')
|
||||
self.schema_api.validate('image', body)
|
||||
self.schema.validate(body)
|
||||
|
||||
# Create a dict of base image properties, with user- and deployer-
|
||||
# defined properties contained in a 'properties' dictionary
|
||||
@ -212,9 +216,9 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||
|
||||
|
||||
class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
def __init__(self, schema_api):
|
||||
def __init__(self, schema=None):
|
||||
super(ResponseSerializer, self).__init__()
|
||||
self.schema_api = schema_api
|
||||
self.schema = schema or get_schema()
|
||||
|
||||
def _get_image_href(self, image, subcollection=''):
|
||||
base_href = '/v2/images/%s' % image['id']
|
||||
@ -229,19 +233,12 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
{'rel': 'describedby', 'href': '/v2/schemas/image'},
|
||||
]
|
||||
|
||||
def _filter_allowed_image_attributes(self, image):
|
||||
schema = self.schema_api.get_schema('image')
|
||||
if schema.get('additionalProperties', True):
|
||||
return dict(image.iteritems())
|
||||
attrs = schema['properties'].keys()
|
||||
return dict((k, v) for (k, v) in image.iteritems() if k in attrs)
|
||||
|
||||
def _format_image(self, image):
|
||||
_image = image['properties']
|
||||
_image = self._filter_allowed_image_attributes(_image)
|
||||
for key in ['id', 'name', 'created_at', 'updated_at', 'tags']:
|
||||
_image[key] = image[key]
|
||||
_image['visibility'] = 'public' if image['is_public'] else 'private'
|
||||
_image = self.schema.filter(_image)
|
||||
_image['links'] = self._get_image_links(image)
|
||||
self._serialize_datetimes(_image)
|
||||
return _image
|
||||
@ -273,9 +270,73 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
||||
response.status_int = 204
|
||||
|
||||
|
||||
def create_resource(schema_api):
|
||||
_BASE_PROPERTIES = {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'maxLength': 255,
|
||||
},
|
||||
'visibility': {
|
||||
'type': 'string',
|
||||
'description': 'Scope of image accessibility',
|
||||
'enum': ['public', 'private'],
|
||||
},
|
||||
'created_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
#TODO(bcwaldon): our jsonschema library doesn't seem to like the
|
||||
# format attribute, figure out why!
|
||||
#'format': 'date-time',
|
||||
},
|
||||
'updated_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of the last image modification',
|
||||
#'format': 'date-time',
|
||||
},
|
||||
'tags': {
|
||||
'type': 'array',
|
||||
'description': 'List of strings related to the image',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'maxLength': 255,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_schema(custom_properties=None):
|
||||
properties = dict(_BASE_PROPERTIES)
|
||||
if CONF.allow_additional_image_properties:
|
||||
schema = glance.schema.PermissiveSchema('image', properties)
|
||||
else:
|
||||
schema = glance.schema.Schema('image', properties)
|
||||
schema.merge_properties(custom_properties or {})
|
||||
return schema
|
||||
|
||||
|
||||
def load_custom_properties():
|
||||
"""Find the schema properties files and load them into a dict."""
|
||||
match = CONF.find_file('schema-image.json')
|
||||
if match:
|
||||
schema_file = open(match)
|
||||
schema_data = schema_file.read()
|
||||
return json.loads(schema_data)
|
||||
else:
|
||||
msg = _('Could not find schema properties file %s. Continuing '
|
||||
'without custom properties')
|
||||
LOG.warn(msg % filename)
|
||||
return {}
|
||||
|
||||
|
||||
def create_resource(custom_properties=None):
|
||||
"""Images resource factory method"""
|
||||
deserializer = RequestDeserializer(schema_api)
|
||||
serializer = ResponseSerializer(schema_api)
|
||||
schema = get_schema(custom_properties)
|
||||
deserializer = RequestDeserializer(schema)
|
||||
serializer = ResponseSerializer(schema)
|
||||
controller = ImagesController()
|
||||
return wsgi.Resource(controller, deserializer, serializer)
|
||||
|
@ -24,7 +24,6 @@ from glance.api.v2 import images
|
||||
from glance.api.v2 import root
|
||||
from glance.api.v2 import schemas
|
||||
from glance.common import wsgi
|
||||
import glance.schema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -34,13 +33,12 @@ class API(wsgi.Router):
|
||||
"""WSGI router for Glance v2 API requests."""
|
||||
|
||||
def __init__(self, mapper):
|
||||
schema_api = glance.schema.API()
|
||||
glance.schema.load_custom_schema_properties(schema_api)
|
||||
custom_image_properties = images.load_custom_properties()
|
||||
|
||||
root_resource = root.create_resource()
|
||||
mapper.connect('/', controller=root_resource, action='index')
|
||||
|
||||
schemas_resource = schemas.create_resource(schema_api)
|
||||
schemas_resource = schemas.create_resource(custom_image_properties)
|
||||
mapper.connect('/schemas',
|
||||
controller=schemas_resource,
|
||||
action='index',
|
||||
@ -54,7 +52,7 @@ class API(wsgi.Router):
|
||||
action='access',
|
||||
conditions={'method': ['GET']})
|
||||
|
||||
images_resource = images.create_resource(schema_api)
|
||||
images_resource = images.create_resource(custom_image_properties)
|
||||
mapper.connect('/images',
|
||||
controller=images_resource,
|
||||
action='index',
|
||||
@ -96,7 +94,7 @@ class API(wsgi.Router):
|
||||
action='delete',
|
||||
conditions={'method': ['DELETE']})
|
||||
|
||||
image_access_resource = image_access.create_resource(schema_api)
|
||||
image_access_resource = image_access.create_resource()
|
||||
mapper.connect('/images/{image_id}/access',
|
||||
controller=image_access_resource,
|
||||
action='index',
|
||||
|
@ -13,13 +13,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glance.api.v2 import images
|
||||
from glance.api.v2 import image_access
|
||||
from glance.common import wsgi
|
||||
import glance.schema
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, schema_api):
|
||||
self.schema_api = schema_api
|
||||
def __init__(self, custom_image_properties=None):
|
||||
self.access_schema = image_access.get_schema()
|
||||
self.image_schema = images.get_schema(custom_image_properties)
|
||||
|
||||
def index(self, req):
|
||||
links = [
|
||||
@ -29,12 +31,12 @@ class Controller(object):
|
||||
return {'links': links}
|
||||
|
||||
def image(self, req):
|
||||
return self.schema_api.get_schema('image')
|
||||
return self.image_schema.raw()
|
||||
|
||||
def access(self, req):
|
||||
return self.schema_api.get_schema('access')
|
||||
return self.access_schema.raw()
|
||||
|
||||
|
||||
def create_resource(schema_api):
|
||||
controller = Controller(schema_api)
|
||||
def create_resource(custom_image_properties=None):
|
||||
controller = Controller(custom_image_properties)
|
||||
return wsgi.Resource(controller)
|
||||
|
145
glance/schema.py
145
glance/schema.py
@ -13,131 +13,60 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
import jsonschema
|
||||
|
||||
from glance.common import exception
|
||||
from glance.openstack.common import cfg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
_BASE_SCHEMA_PROPERTIES = {
|
||||
'image': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'maxLength': 255,
|
||||
},
|
||||
'visibility': {
|
||||
'type': 'string',
|
||||
'description': 'Scope of image accessibility',
|
||||
'enum': ['public', 'private'],
|
||||
},
|
||||
'created_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
#TODO(bcwaldon): our jsonschema library doesn't seem to like the
|
||||
# format attribute, figure out why!
|
||||
#'format': 'date-time',
|
||||
},
|
||||
'updated_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of the last image modification',
|
||||
#'format': 'date-time',
|
||||
},
|
||||
'tags': {
|
||||
'type': 'array',
|
||||
'description': 'List of strings related to the image',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'maxLength': 255,
|
||||
},
|
||||
},
|
||||
},
|
||||
'access': {
|
||||
'tenant_id': {
|
||||
'type': 'string',
|
||||
'description': 'The tenant identifier',
|
||||
},
|
||||
'can_share': {
|
||||
'type': 'boolean',
|
||||
'description': 'Ability of tenant to share with others',
|
||||
'default': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class API(object):
|
||||
def __init__(self, base_properties=_BASE_SCHEMA_PROPERTIES):
|
||||
self.base_properties = base_properties
|
||||
self.schema_properties = copy.deepcopy(self.base_properties)
|
||||
class Schema(object):
|
||||
|
||||
def get_schema(self, name):
|
||||
if name == 'image' and CONF.allow_additional_image_properties:
|
||||
additional = {'type': 'string'}
|
||||
else:
|
||||
additional = False
|
||||
return {
|
||||
'name': name,
|
||||
'properties': self.schema_properties[name],
|
||||
'additionalProperties': additional
|
||||
}
|
||||
def __init__(self, name, properties=None):
|
||||
self.name = name
|
||||
if properties is None:
|
||||
properties = {}
|
||||
self.properties = properties
|
||||
|
||||
def set_custom_schema_properties(self, schema_name, custom_properties):
|
||||
"""Update the custom properties of a schema with those provided."""
|
||||
schema_properties = copy.deepcopy(self.base_properties[schema_name])
|
||||
def validate(self, obj):
|
||||
try:
|
||||
jsonschema.validate(obj, self.raw())
|
||||
except jsonschema.ValidationError as e:
|
||||
raise exception.InvalidObject(schema=self.name, reason=str(e))
|
||||
|
||||
def filter(self, obj):
|
||||
filtered = {}
|
||||
for key, value in obj.iteritems():
|
||||
if key in self.properties:
|
||||
filtered[key] = value
|
||||
return filtered
|
||||
|
||||
def merge_properties(self, properties):
|
||||
# Ensure custom props aren't attempting to override base props
|
||||
base_keys = set(schema_properties.keys())
|
||||
custom_keys = set(custom_properties.keys())
|
||||
intersecting_keys = base_keys.intersection(custom_keys)
|
||||
original_keys = set(self.properties.keys())
|
||||
new_keys = set(properties.keys())
|
||||
intersecting_keys = original_keys.intersection(new_keys)
|
||||
conflicting_keys = [k for k in intersecting_keys
|
||||
if schema_properties[k] != custom_properties[k]]
|
||||
if self.properties[k] != properties[k]]
|
||||
if len(conflicting_keys) > 0:
|
||||
props = ', '.join(conflicting_keys)
|
||||
reason = _("custom properties (%(props)s) conflict "
|
||||
"with base properties")
|
||||
raise exception.SchemaLoadError(reason=reason % {'props': props})
|
||||
|
||||
schema_properties.update(copy.deepcopy(custom_properties))
|
||||
self.schema_properties[schema_name] = schema_properties
|
||||
self.properties.update(properties)
|
||||
|
||||
def validate(self, schema_name, obj):
|
||||
schema = self.get_schema(schema_name)
|
||||
try:
|
||||
jsonschema.validate(obj, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise exception.InvalidObject(schema=schema_name, reason=str(e))
|
||||
def raw(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'properties': self.properties,
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
|
||||
def read_schema_properties_file(schema_name):
|
||||
"""Find the schema properties files and load them into a dict."""
|
||||
schema_filename = 'schema-%s.json' % schema_name
|
||||
match = CONF.find_file(schema_filename)
|
||||
if match:
|
||||
schema_file = open(match)
|
||||
schema_data = schema_file.read()
|
||||
return json.loads(schema_data)
|
||||
else:
|
||||
msg = _('Could not find schema properties file %s. Continuing '
|
||||
'without custom properties')
|
||||
logger.warn(msg % schema_filename)
|
||||
return {}
|
||||
class PermissiveSchema(Schema):
|
||||
def filter(self, obj):
|
||||
return obj
|
||||
|
||||
|
||||
def load_custom_schema_properties(api):
|
||||
"""Extend base image and access schemas with custom properties."""
|
||||
for schema_name in ('image', 'access'):
|
||||
image_properties = read_schema_properties_file(schema_name)
|
||||
api.set_custom_schema_properties(schema_name, image_properties)
|
||||
def raw(self):
|
||||
raw = super(PermissiveSchema, self).raw()
|
||||
raw['additionalProperties'] = {'type': 'string'}
|
||||
return raw
|
||||
|
@ -1,43 +0,0 @@
|
||||
# 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.schema
|
||||
from glance.tests import utils
|
||||
|
||||
|
||||
class TestSchemaAPI(utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSchemaAPI, self).setUp()
|
||||
self.schema_api = glance.schema.API()
|
||||
|
||||
def test_load_image_schema(self):
|
||||
output = self.schema_api.get_schema('image')
|
||||
self.assertEqual('image', output['name'])
|
||||
expected_keys = set([
|
||||
'id',
|
||||
'name',
|
||||
'visibility',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'tags',
|
||||
])
|
||||
self.assertEqual(expected_keys, set(output['properties'].keys()))
|
||||
|
||||
def test_load_access_schema(self):
|
||||
output = self.schema_api.get_schema('access')
|
||||
self.assertEqual('access', output['name'])
|
||||
expected_keys = ['tenant_id', 'can_share']
|
||||
self.assertEqual(expected_keys, output['properties'].keys())
|
@ -18,226 +18,94 @@ import glance.schema
|
||||
from glance.tests import utils as test_utils
|
||||
|
||||
|
||||
FAKE_BASE_PROPERTIES = {
|
||||
'fake1': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
'image': {
|
||||
'gazump': {
|
||||
'type': 'string',
|
||||
'description': 'overcharge; rip off',
|
||||
'required': False,
|
||||
},
|
||||
'cumulus': {
|
||||
'type': 'string',
|
||||
'description': 'a heap; pile',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestSchemaAPI(test_utils.BaseTestCase):
|
||||
class TestBasicSchema(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSchemaAPI, self).setUp()
|
||||
self.schema_api = glance.schema.API(FAKE_BASE_PROPERTIES)
|
||||
|
||||
def test_get_schema(self):
|
||||
output = self.schema_api.get_schema('fake1')
|
||||
expected = {
|
||||
'name': 'fake1',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
super(TestBasicSchema, self).setUp()
|
||||
properties = {
|
||||
'ham': {'type': 'string'},
|
||||
'eggs': {'type': 'string'},
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
self.schema = glance.schema.Schema('basic', properties)
|
||||
|
||||
def test_get_schema_after_load(self):
|
||||
extra_props = {
|
||||
'prop1': {
|
||||
'type': 'string',
|
||||
'description': 'Just some property',
|
||||
'required': False,
|
||||
'maxLength': 128,
|
||||
},
|
||||
}
|
||||
def test_validate_passes(self):
|
||||
obj = {'ham': 'no', 'eggs': 'scrambled'}
|
||||
self.schema.validate(obj) # No exception raised
|
||||
|
||||
self.schema_api.set_custom_schema_properties('fake1', extra_props)
|
||||
output = self.schema_api.get_schema('fake1')
|
||||
def test_validate_fails_on_extra_properties(self):
|
||||
obj = {'ham': 'virginia', 'eggs': 'scrambled', 'bacon': 'crispy'}
|
||||
self.assertRaises(exception.InvalidObject, self.schema.validate, obj)
|
||||
|
||||
expected = {
|
||||
'name': 'fake1',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
'prop1': {
|
||||
'type': 'string',
|
||||
'description': 'Just some property',
|
||||
'required': False,
|
||||
'maxLength': 128,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
def test_validate_fails_on_bad_type(self):
|
||||
obj = {'eggs': 2}
|
||||
self.assertRaises(exception.InvalidObject, self.schema.validate, obj)
|
||||
|
||||
def test_get_schema_load_conflict(self):
|
||||
extra_props = {
|
||||
'name': {
|
||||
'type': 'int',
|
||||
'description': 'Descriptive integer for the image',
|
||||
'required': False,
|
||||
},
|
||||
}
|
||||
def test_filter_strips_extra_properties(self):
|
||||
obj = {'ham': 'virginia', 'eggs': 'scrambled', 'bacon': 'crispy'}
|
||||
filtered = self.schema.filter(obj)
|
||||
expected = {'ham': 'virginia', 'eggs': 'scrambled'}
|
||||
self.assertEqual(filtered, expected)
|
||||
|
||||
def test_merge_properties(self):
|
||||
self.schema.merge_properties({'bacon': {'type': 'string'}})
|
||||
expected = set(['ham', 'eggs', 'bacon'])
|
||||
actual = set(self.schema.raw()['properties'].keys())
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_merge_conflicting_properties(self):
|
||||
conflicts = {'eggs': {'type': 'integer'}}
|
||||
self.assertRaises(exception.SchemaLoadError,
|
||||
self.schema_api.set_custom_schema_properties,
|
||||
'fake1',
|
||||
extra_props)
|
||||
self.schema.merge_properties, conflicts)
|
||||
|
||||
# Schema should not have changed due to the conflict
|
||||
output = self.schema_api.get_schema('fake1')
|
||||
def test_merge_conflicting_but_identical_properties(self):
|
||||
conflicts = {'ham': {'type': 'string'}}
|
||||
self.schema.merge_properties(conflicts) # no exception raised
|
||||
expected = set(['ham', 'eggs'])
|
||||
actual = set(self.schema.raw()['properties'].keys())
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_raw_json_schema(self):
|
||||
expected = {
|
||||
'name': 'fake1',
|
||||
'name': 'basic',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
'ham': {'type': 'string'},
|
||||
'eggs': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
self.assertEqual(self.schema.raw(), expected)
|
||||
|
||||
def test_get_schema_load_conflict_base_property(self):
|
||||
extra_props = {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
|
||||
class TestPermissiveSchema(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPermissiveSchema, self).setUp()
|
||||
properties = {
|
||||
'ham': {'type': 'string'},
|
||||
'eggs': {'type': 'string'},
|
||||
}
|
||||
self.schema = glance.schema.PermissiveSchema('permissive', properties)
|
||||
|
||||
# Schema update should not raise an exception, but it should also
|
||||
# remain unchanged
|
||||
self.schema_api.set_custom_schema_properties('fake1', extra_props)
|
||||
output = self.schema_api.get_schema('fake1')
|
||||
expected = {
|
||||
'name': 'fake1',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
def test_validate_with_additional_properties_allowed(self):
|
||||
obj = {'ham': 'virginia', 'eggs': 'scrambled', 'bacon': 'crispy'}
|
||||
self.schema.validate(obj) # No exception raised
|
||||
|
||||
def test_get_image_schema_with_additional_properties_disabled(self):
|
||||
self.config(allow_additional_image_properties=False)
|
||||
output = self.schema_api.get_schema('image')
|
||||
expected = {
|
||||
'name': 'image',
|
||||
'properties': {
|
||||
'gazump': {
|
||||
'type': 'string',
|
||||
'description': 'overcharge; rip off',
|
||||
'required': False,
|
||||
},
|
||||
'cumulus': {
|
||||
'type': 'string',
|
||||
'description': 'a heap; pile',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
def test_validate_rejects_non_string_extra_properties(self):
|
||||
obj = {'ham': 'virginia', 'eggs': 'scrambled', 'grits': 1000}
|
||||
self.assertRaises(exception.InvalidObject, self.schema.validate, obj)
|
||||
|
||||
def test_get_image_schema_with_additional_properties_enabled(self):
|
||||
self.config(allow_additional_image_properties=True)
|
||||
output = self.schema_api.get_schema('image')
|
||||
def test_filter_passes_extra_properties(self):
|
||||
obj = {'ham': 'virginia', 'eggs': 'scrambled', 'bacon': 'crispy'}
|
||||
filtered = self.schema.filter(obj)
|
||||
self.assertEqual(filtered, obj)
|
||||
|
||||
def test_raw_json_schema(self):
|
||||
expected = {
|
||||
'name': 'image',
|
||||
'name': 'permissive',
|
||||
'properties': {
|
||||
'gazump': {
|
||||
'type': 'string',
|
||||
'description': 'overcharge; rip off',
|
||||
'required': False,
|
||||
},
|
||||
'cumulus': {
|
||||
'type': 'string',
|
||||
'description': 'a heap; pile',
|
||||
'required': True,
|
||||
},
|
||||
'ham': {'type': 'string'},
|
||||
'eggs': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': {'type': 'string'},
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
def test_get_other_schema_with_additional_image_properties_enabled(self):
|
||||
self.config(allow_additional_image_properties=False)
|
||||
output = self.schema_api.get_schema('fake1')
|
||||
expected = {
|
||||
'name': 'fake1',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'An identifier for the image',
|
||||
'required': False,
|
||||
'maxLength': 36,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Descriptive name for the image',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
self.assertEqual(output, expected)
|
||||
self.assertEqual(self.schema.raw(), expected)
|
||||
|
@ -17,7 +17,7 @@ import json
|
||||
|
||||
import webob
|
||||
|
||||
from glance.api.v2 import image_access
|
||||
import glance.api.v2.image_access
|
||||
from glance.common import exception
|
||||
from glance.common import utils
|
||||
import glance.schema
|
||||
@ -30,7 +30,7 @@ class TestImageAccessController(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestImageAccessController, self).setUp()
|
||||
self.db = unit_test_utils.FakeDB()
|
||||
self.controller = image_access.Controller(self.db)
|
||||
self.controller = glance.api.v2.image_access.Controller(self.db)
|
||||
|
||||
def test_index(self):
|
||||
req = unit_test_utils.get_fake_request()
|
||||
@ -111,8 +111,7 @@ class TestImageAccessDeserializer(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImageAccessDeserializer, self).setUp()
|
||||
schema_api = glance.schema.API()
|
||||
self.deserializer = image_access.RequestDeserializer(schema_api)
|
||||
self.deserializer = glance.api.v2.image_access.RequestDeserializer()
|
||||
|
||||
def test_create(self):
|
||||
fixture = {
|
||||
@ -131,53 +130,8 @@ class TestImageAccessDeserializer(test_utils.BaseTestCase):
|
||||
self.assertEqual(expected, output)
|
||||
|
||||
|
||||
class TestImageAccessDeserializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImageAccessDeserializerWithExtendedSchema, self).setUp()
|
||||
schema_api = glance.schema.API()
|
||||
props = {
|
||||
'color': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'enum': ['blue', 'red'],
|
||||
},
|
||||
}
|
||||
schema_api.set_custom_schema_properties('access', props)
|
||||
self.deserializer = image_access.RequestDeserializer(schema_api)
|
||||
|
||||
def test_create(self):
|
||||
fixture = {
|
||||
'tenant_id': unit_test_utils.TENANT1,
|
||||
'can_share': False,
|
||||
'color': 'blue',
|
||||
}
|
||||
expected = {
|
||||
'access_record': {
|
||||
'member': unit_test_utils.TENANT1,
|
||||
'can_share': False,
|
||||
'color': 'blue',
|
||||
},
|
||||
}
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps(fixture)
|
||||
output = self.deserializer.create(request)
|
||||
self.assertEqual(expected, output)
|
||||
|
||||
def test_create_bad_data(self):
|
||||
fixture = {
|
||||
'tenant_id': unit_test_utils.TENANT1,
|
||||
'can_share': False,
|
||||
'color': 'purple',
|
||||
}
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps(fixture)
|
||||
self.assertRaises(exception.InvalidObject,
|
||||
self.deserializer.create, request)
|
||||
|
||||
|
||||
class TestImageAccessSerializer(test_utils.BaseTestCase):
|
||||
serializer = image_access.ResponseSerializer()
|
||||
serializer = glance.api.v2.image_access.ResponseSerializer()
|
||||
|
||||
def test_show(self):
|
||||
fixture = {
|
||||
|
@ -30,8 +30,7 @@ class TestImagesController(base.StoreClearingUnitTest):
|
||||
|
||||
self.config(verbose=True, debug=True)
|
||||
|
||||
controller_class = glance.api.v2.image_data.ImageDataController
|
||||
self.controller = controller_class(
|
||||
self.controller = glance.api.v2.image_data.ImageDataController(
|
||||
db_api=unit_test_utils.FakeDB(),
|
||||
store_api=unit_test_utils.FakeStoreAPI())
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import stubout
|
||||
import webob
|
||||
|
||||
import glance.api.v2.images
|
||||
@ -250,9 +251,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImagesDeserializer, self).setUp()
|
||||
schema_api = glance.schema.API()
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer(
|
||||
schema_api)
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer()
|
||||
|
||||
def test_create_with_id(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
@ -392,17 +391,16 @@ class TestImagesDeserializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImagesDeserializerWithExtendedSchema, self).setUp()
|
||||
schema_api = glance.schema.API()
|
||||
props = {
|
||||
self.config(allow_additional_image_properties=False)
|
||||
custom_image_properties = {
|
||||
'pants': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'enum': ['on', 'off'],
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'enum': ['on', 'off'],
|
||||
},
|
||||
}
|
||||
schema_api.set_custom_schema_properties('image', props)
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer(
|
||||
schema_api)
|
||||
schema = glance.api.v2.images.get_schema(custom_image_properties)
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer(schema)
|
||||
|
||||
def test_create(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
@ -446,9 +444,7 @@ class TestImagesDeserializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestImagesDeserializerWithAdditionalProperties, self).setUp()
|
||||
self.config(allow_additional_image_properties=True)
|
||||
schema_api = glance.schema.API()
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer(
|
||||
schema_api)
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer()
|
||||
|
||||
def test_create(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
@ -457,13 +453,6 @@ class TestImagesDeserializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
expected = {'image': {'properties': {'foo': 'bar'}}}
|
||||
self.assertEqual(expected, output)
|
||||
|
||||
def test_create_with_additional_properties_disallowed(self):
|
||||
self.config(allow_additional_image_properties=False)
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps({'foo': 'bar'})
|
||||
self.assertRaises(exception.InvalidObject,
|
||||
self.deserializer.create, request)
|
||||
|
||||
def test_create_with_numeric_property(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps({'abc': 123})
|
||||
@ -483,8 +472,22 @@ class TestImagesDeserializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
expected = {'image': {'properties': {'foo': 'bar'}}}
|
||||
self.assertEqual(expected, output)
|
||||
|
||||
def test_update_with_additional_properties_disallowed(self):
|
||||
|
||||
class TestImagesDeserializerNoAdditionalProperties(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImagesDeserializerNoAdditionalProperties, self).setUp()
|
||||
self.config(allow_additional_image_properties=False)
|
||||
self.deserializer = glance.api.v2.images.RequestDeserializer()
|
||||
|
||||
def test_create_with_additional_properties_disallowed(self):
|
||||
self.config(allow_additional_image_properties=False)
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps({'foo': 'bar'})
|
||||
self.assertRaises(exception.InvalidObject,
|
||||
self.deserializer.create, request)
|
||||
|
||||
def test_update(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
request.body = json.dumps({'foo': 'bar'})
|
||||
self.assertRaises(exception.InvalidObject,
|
||||
@ -495,8 +498,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestImagesSerializer, self).setUp()
|
||||
schema_api = glance.schema.API()
|
||||
self.serializer = glance.api.v2.images.ResponseSerializer(schema_api)
|
||||
self.serializer = glance.api.v2.images.ResponseSerializer()
|
||||
|
||||
def test_index(self):
|
||||
fixtures = [
|
||||
@ -672,15 +674,16 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestImagesSerializerWithExtendedSchema, self).setUp()
|
||||
self.config(allow_additional_image_properties=False)
|
||||
self.schema_api = glance.schema.API()
|
||||
props = {
|
||||
custom_image_properties = {
|
||||
'color': {
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'enum': ['red', 'green'],
|
||||
},
|
||||
}
|
||||
self.schema_api.set_custom_schema_properties('image', props)
|
||||
schema = glance.api.v2.images.get_schema(custom_image_properties)
|
||||
self.serializer = glance.api.v2.images.ResponseSerializer(schema)
|
||||
|
||||
self.fixture = {
|
||||
'id': unit_test_utils.UUID2,
|
||||
'name': 'image-2',
|
||||
@ -692,7 +695,6 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
}
|
||||
|
||||
def test_show(self):
|
||||
serializer = glance.api.v2.images.ResponseSerializer(self.schema_api)
|
||||
expected = {
|
||||
'image': {
|
||||
'id': unit_test_utils.UUID2,
|
||||
@ -716,11 +718,10 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
},
|
||||
}
|
||||
response = webob.Response()
|
||||
serializer.show(response, self.fixture)
|
||||
self.serializer.show(response, self.fixture)
|
||||
self.assertEqual(expected, json.loads(response.body))
|
||||
|
||||
def test_show_reports_invalid_data(self):
|
||||
serializer = glance.api.v2.images.ResponseSerializer(self.schema_api)
|
||||
self.fixture['properties']['color'] = 'invalid'
|
||||
expected = {
|
||||
'image': {
|
||||
@ -745,7 +746,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
|
||||
},
|
||||
}
|
||||
response = webob.Response()
|
||||
serializer.show(response, self.fixture)
|
||||
self.serializer.show(response, self.fixture)
|
||||
self.assertEqual(expected, json.loads(response.body))
|
||||
|
||||
|
||||
@ -754,7 +755,6 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestImagesSerializerWithAdditionalProperties, self).setUp()
|
||||
self.config(allow_additional_image_properties=True)
|
||||
self.schema_api = glance.schema.API()
|
||||
self.fixture = {
|
||||
'id': unit_test_utils.UUID2,
|
||||
'name': 'image-2',
|
||||
@ -768,7 +768,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
}
|
||||
|
||||
def test_show(self):
|
||||
serializer = glance.api.v2.images.ResponseSerializer(self.schema_api)
|
||||
serializer = glance.api.v2.images.ResponseSerializer()
|
||||
expected = {
|
||||
'image': {
|
||||
'id': unit_test_utils.UUID2,
|
||||
@ -799,7 +799,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
"""Ensure that the serializer passes through invalid additional
|
||||
properties (i.e. non-string) without complaining.
|
||||
"""
|
||||
serializer = glance.api.v2.images.ResponseSerializer(self.schema_api)
|
||||
serializer = glance.api.v2.images.ResponseSerializer()
|
||||
self.fixture['properties']['marx'] = 123
|
||||
expected = {
|
||||
'image': {
|
||||
@ -829,7 +829,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
|
||||
|
||||
def test_show_with_additional_properties_disabled(self):
|
||||
self.config(allow_additional_image_properties=False)
|
||||
serializer = glance.api.v2.images.ResponseSerializer(self.schema_api)
|
||||
serializer = glance.api.v2.images.ResponseSerializer()
|
||||
expected = {
|
||||
'image': {
|
||||
'id': unit_test_utils.UUID2,
|
||||
|
@ -13,8 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glance.api.v2 import schemas
|
||||
import glance.schema
|
||||
import glance.api.v2.schemas
|
||||
import glance.tests.unit.utils as unit_test_utils
|
||||
import glance.tests.utils as test_utils
|
||||
|
||||
@ -23,8 +22,7 @@ class TestSchemasController(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSchemasController, self).setUp()
|
||||
self.schema_api = glance.schema.API()
|
||||
self.controller = schemas.Controller(self.schema_api)
|
||||
self.controller = glance.api.v2.schemas.Controller()
|
||||
|
||||
def test_index(self):
|
||||
req = unit_test_utils.get_fake_request()
|
||||
@ -38,9 +36,9 @@ class TestSchemasController(test_utils.BaseTestCase):
|
||||
def test_image(self):
|
||||
req = unit_test_utils.get_fake_request()
|
||||
output = self.controller.image(req)
|
||||
self.assertEqual(self.schema_api.get_schema('image'), output)
|
||||
self.assertEqual(output['name'], 'image')
|
||||
|
||||
def test_access(self):
|
||||
req = unit_test_utils.get_fake_request()
|
||||
output = self.controller.access(req)
|
||||
self.assertEqual(self.schema_api.get_schema('access'), output)
|
||||
self.assertEqual(output['name'], 'access')
|
||||
|
Loading…
Reference in New Issue
Block a user