Create DesignateAdapter and surrounding infrastructure

This implements the basic infrastructure for DesignateAdapter
and creates the basis for the API v2/v1 Adapter objects

These will replace views in both the v1 and v2 APIs, and allow for the
validation of API requests to move into the DesignateObjects themselves.

Example use is

   zone = central.get_domain(blah)
   DesignateAdapter.render('API_v1', zone)
   // Returns v1 dict, ready to	convert to JSON

These objects have a registry of sorts, so all calls will be made to
DesignateAdapter, and the combination of the format param ('API_v1' in
this case) will load the right Adapter and return the right JSON ready
dict.

Change-Id: I2b77205751675de600248180fafbfe22a2c1d8f5
Blueprint: validation-cleanup
This commit is contained in:
Graham Hayes 2015-03-04 17:29:51 +00:00
parent 95adcfcc4d
commit f7252ecdb0
8 changed files with 479 additions and 0 deletions

View File

@ -39,6 +39,11 @@ class RelationNotLoaded(Base):
error_type = 'relation_not_loaded' error_type = 'relation_not_loaded'
class AdapterNotFound(Base):
error_code = 500
error_type = 'adapter_not_found'
class NSD4SlaveBackendError(Backend): class NSD4SlaveBackendError(Backend):
pass pass

View File

@ -0,0 +1,15 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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.
# Base Adapter Class
from designate.objects.adapters.base import DesignateAdapter # noqa

View File

@ -0,0 +1,59 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 oslo_log import log as logging
from designate.objects.adapters import base
LOG = logging.getLogger(__name__)
class APIv1Adapter(base.DesignateAdapter):
ADAPTER_FORMAT = 'API_v1'
#####################
# Rendering methods #
#####################
@classmethod
def render(cls, object, *args, **kwargs):
return super(APIv1Adapter, cls).render(
cls.ADAPTER_FORMAT, object, *args, **kwargs)
@classmethod
def _render_list(cls, list_object, *args, **kwargs):
inner = cls._render_inner_list(list_object, *args, **kwargs)
return {cls.MODIFICATIONS['options']['collection_name']: inner}
@classmethod
def _render_object(cls, object, *args, **kwargs):
return cls._render_inner_object(object, *args, **kwargs)
#####################
# Parsing methods #
#####################
@classmethod
def parse(cls, values, output_object, *args, **kwargs):
return super(APIv1Adapter, cls).parse(
cls.ADAPTER_FORMAT, values, output_object, *args, **kwargs)
@classmethod
def _parse_list(cls, values, output_object, *args, **kwargs):
return cls._parse_inner_list(values, output_object, *args, **kwargs)
@classmethod
def _parse_object(cls, values, output_object, *args, **kwargs):
return cls._parse_inner_object(values, output_object, *args, **kwargs)

View File

@ -0,0 +1,141 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 urllib
from oslo_log import log as logging
from oslo.config import cfg
from designate.objects.adapters import base
from designate.objects import base as obj_base
LOG = logging.getLogger(__name__)
cfg.CONF.import_opt('api_base_uri', 'designate.api', group='service:api')
class APIv2Adapter(base.DesignateAdapter):
BASE_URI = cfg.CONF['service:api'].api_base_uri.rstrip('/')
ADAPTER_FORMAT = 'API_v2'
#####################
# Rendering methods #
#####################
@classmethod
def render(cls, object, *args, **kwargs):
return super(APIv2Adapter, cls).render(
cls.ADAPTER_FORMAT, object, *args, **kwargs)
@classmethod
def _render_list(cls, list_object, *args, **kwargs):
inner = cls._render_inner_list(list_object, *args, **kwargs)
outer = {}
if cls.MODIFICATIONS['options'].get('links', True):
outer['links'] = cls._get_collection_links(
list_object, kwargs['request'])
# Check if we should include metadata
if isinstance(list_object, obj_base.PagedListObjectMixin):
metadata = {}
metadata['total_count'] = list_object.total_count
outer['metadata'] = metadata
outer[cls.MODIFICATIONS['options']['collection_name']] = inner
return outer
@classmethod
def _render_object(cls, object, *args, **kwargs):
inner = cls._render_inner_object(object, *args, **kwargs)
if cls.MODIFICATIONS['options'].get('links', True):
inner['links'] = cls._get_resource_links(object, kwargs['request'])
return {cls.MODIFICATIONS['options']['resource_name']: inner}
#####################
# Parsing methods #
#####################
@classmethod
def parse(cls, values, output_object, *args, **kwargs):
return super(APIv2Adapter, cls).parse(
cls.ADAPTER_FORMAT, values, output_object, *args, **kwargs)
@classmethod
def _parse_list(cls, values, output_object, *args, **kwargs):
return cls._parse_inner_list(values, output_object, *args, **kwargs)
@classmethod
def _parse_object(cls, values, output_object, *args, **kwargs):
inner = values[cls.MODIFICATIONS['options']['resource_name']]
return cls._parse_inner_object(inner, output_object, *args, **kwargs)
#####################
# Link methods #
#####################
@classmethod
def _get_resource_links(cls, object, request):
return {'self': '%s%s/%s' %
(cls.BASE_URI, cls._get_path(request), object.id)}
@classmethod
def _get_path(cls, request):
path = request.path.lstrip('/').split('/')
item_path = ''
for part in path:
if part == cls.MODIFICATIONS['options']['collection_name']:
item_path += '/' + part
return item_path
else:
item_path += '/' + part
@classmethod
def _get_collection_links(cls, list, request):
links = {
'self': cls._get_collection_href(request)
}
params = request.GET
if 'limit' in params and int(params['limit']) == len(list):
links['next'] = cls._get_next_href(request, list)
return links
@classmethod
def _get_collection_href(cls, request, extra_params=None):
params = request.GET
if extra_params is not None:
params.update(extra_params)
href = "%s%s?%s" % (
cls.BASE_URI,
cls._get_path(request),
urllib.urlencode(params))
return href.rstrip('?')
@classmethod
def _get_next_href(cls, request, items):
# Prepare the extra params
extra_params = {
'marker': items[-1]['id']
}
return cls._get_collection_href(request, extra_params)

View File

@ -0,0 +1,200 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 six
from designate import objects
from designate import exceptions
LOG = logging.getLogger(__name__)
class DesignateObjectAdapterMetaclass(type):
def __init__(cls, names, bases, dict_):
if not hasattr(cls, '_adapter_classes'):
cls._adapter_classes = {}
return
key = '%s:%s' % (cls.adapter_format(), cls.adapter_object())
if key not in cls._adapter_classes:
cls._adapter_classes[key] = cls
else:
raise Exception(
"Duplicate DesignateAdapterObject with"
" format '%(format)s and object %(object)s'" %
{'format': cls.adapter_format(),
'object': cls.adapter_object()}
)
@six.add_metaclass(DesignateObjectAdapterMetaclass)
class DesignateAdapter(object):
"""docstring for DesignateObjectAdapter"""
ADAPTER_OBJECT = objects.DesignateObject
@classmethod
def adapter_format(cls):
return cls.ADAPTER_FORMAT
@classmethod
def adapter_object(cls):
return cls.ADAPTER_OBJECT.obj_name()
@classmethod
def get_object_adapter(cls, format_, object):
key = '%s:%s' % (format_, object.obj_name())
try:
return cls._adapter_classes[key]
except KeyError as e:
keys = e.message.split(':')
msg = "Adapter for %(object)s to format %(format)s not found" % {
"object": keys[1],
"format": keys[0]
}
raise exceptions.AdapterNotFound(msg)
#####################
# Rendering methods #
#####################
@classmethod
def render(cls, format_, object, *args, **kwargs):
if isinstance(object, objects.ListObjectMixin):
# type_ = 'list'
return cls.get_object_adapter(
format_, object)._render_list(object, *args, **kwargs)
else:
# type_ = 'object'
return cls.get_object_adapter(
format_, object)._render_object(object, *args, **kwargs)
@classmethod
def _render_inner_object(cls, object, *args, **kwargs):
# The dict we will return to be rendered to JSON / output format
r_obj = {}
# Loop over all fields that are supposed to be output
for key, value in cls.MODIFICATIONS['fields'].iteritems():
# Get properties for this field
field_props = cls.MODIFICATIONS['fields'][key]
# Check if it has to be renamed
if field_props.get('rename', False):
obj = getattr(object, field_props.get('rename'))
# if rename is specified we need to change the key
obj_key = field_props.get('rename')
else:
# if not, move on
obj = getattr(object, key, None)
obj_key = key
# Check if this item is a relation (another DesignateObject that
# will need to be converted itself
if object.FIELDS.get(obj_key, {}).get('relation'):
# Get a adapter for the nested object
# Get the class the object is and get its adapter, then set
# the item in the dict to the output
r_obj[key] = cls.get_object_adapter(
cls.ADAPTER_FORMAT,
object.FIELDS[obj_key].get('relation_cls')).render(
obj, *args, **kwargs)
else:
# Just attach the damn item if there is no weird edge cases
r_obj[key] = obj
# Send it back
return r_obj
@classmethod
def _render_inner_list(cls, list_object, *args, **kwargs):
# The list we will return to be rendered to JSON / output format
r_list = []
# iterate and convert each DesignateObject in the list, and append to
# the object we are returning
for object in list_object:
r_list.append(cls.get_object_adapter(
cls.ADAPTER_FORMAT,
object.obj_name()).render(object, *args, **kwargs))
return r_list
#####################
# Parsing methods #
#####################
@classmethod
def parse(cls, format_, values, output_object, *args, **kwargs):
if isinstance(output_object, objects.ListObjectMixin):
# type_ = 'list'
return cls.get_object_adapter(
format_,
output_object.obj_name())._parse_list(
values, output_object, *args, **kwargs)
else:
# type_ = 'object'
return cls.get_object_adapter(
format_,
output_object.obj_name())._parse_object(
values, output_object, *args, **kwargs)
@classmethod
def _parse_inner_object(cls, values, output_object, *args, **kwargs):
error_keys = []
for key, value in values.iteritems():
error_flag = True
if key in cls.MODIFICATIONS['fields']:
# No rename needed
obj_key = key
error_flag = False
# This item may need to be translated
if cls.MODIFICATIONS['fields'][key].get('rename', False):
obj_key = cls.MODIFICATIONS['fields'][key].get('rename')
# Check if the key is a nested object
if output_object.FIELDS.get(obj_key, {}).get('relation', False):
# Get the right class name
obj_class_name = output_object.FIELDS.get(
obj_key, {}).get('relation_cls')
# Get the an instance of it
obj_class = \
objects.DesignateObject.obj_cls_from_name(obj_class_name)
# Get the adapted object
obj = \
cls.get_object_adapter(
cls.ADAPTER_FORMAT, obj_class_name).parse(
value, obj_class)
# Set the object on the main object
setattr(output_object, obj_key, obj)
else:
# No nested objects here, just set the value
setattr(output_object, obj_key, value)
if error_flag:
# We got an extra key
error_keys.append(key)
if error_keys:
error_message = str.format(
'Provided object does not match schema. Keys {0} are not '
'valid in the request body',
error_keys)
raise exceptions.InvalidObject(error_message)
return output_object
@classmethod
def _parse_inner_list(cls, values, output_object, *args, **kwargs):
raise exceptions.NotImplemented('List adaption not implemented')

View File

@ -0,0 +1,59 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@hp.com>
#
# 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 oslo_log import log as logging
from designate import tests
from designate import objects
from designate.objects import adapters
LOG = logging.getLogger(__name__)
class DesignateTestAdapter(adapters.DesignateAdapter):
ADAPTER_OBJECT = objects.DesignateObject
ADAPTER_FORMAT = 'TEST_API'
MODIFICATIONS = {
'fields': {},
'options': {}
}
@classmethod
def render(cls, object, *args, **kwargs):
return super(DesignateTestAdapter, cls).render(
cls.ADAPTER_FORMAT, object, *args, **kwargs)
@classmethod
def _render_list(cls, list_object, *args, **kwargs):
inner = cls._render_inner_list(list_object, *args, **kwargs)
return inner
@classmethod
def _render_object(cls, object, *args, **kwargs):
inner = cls._render_inner_object(object, *args, **kwargs)
return inner
class DesignateAdapterTest(tests.TestCase):
def test_get_object_adapter(self):
adapters.DesignateAdapter.get_object_adapter(
'TEST_API', objects.DesignateObject)
def test_get_object_render(self):
adapters.DesignateAdapter.render('TEST_API', objects.DesignateObject)