summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-08-27 12:51:23 +0000
committerGerrit Code Review <review@openstack.org>2013-08-27 12:51:23 +0000
commitc8ad7827b78ecdf516fef8714264f9d73db800ea (patch)
tree0d2642a991e5bcc1cfb33e8b1c78fbf6232dff64
parent2aa6f0fb0b3d2709c6fe12a0d09250066b276234 (diff)
parent24ca94cda8ee756e2fba7e9b5b1dd0375100166e (diff)
downloaddesignate-c8ad7827b78ecdf516fef8714264f9d73db800ea.tar.gz
Merge "Add APIv2 Zones Controller"
-rw-r--r--designate/api/__init__.py1
-rw-r--r--designate/api/middleware.py75
-rw-r--r--designate/api/v1/__init__.py71
-rw-r--r--designate/api/v2/__init__.py1
-rw-r--r--designate/api/v2/controllers/recordsets.py49
-rw-r--r--designate/api/v2/controllers/rest.py130
-rw-r--r--designate/api/v2/controllers/root.py12
-rw-r--r--designate/api/v2/controllers/zones.py149
-rw-r--r--designate/api/v2/patches.py38
-rw-r--r--designate/api/v2/views/__init__.py0
-rw-r--r--designate/api/v2/views/base.py118
-rw-r--r--designate/api/v2/views/zones.py60
-rw-r--r--designate/resources/schemas/v2/recordset.json22
-rw-r--r--designate/resources/schemas/v2/zone.json22
-rw-r--r--designate/tests/__init__.py4
-rw-r--r--designate/tests/test_api/test_v1/__init__.py3
-rw-r--r--designate/tests/test_api/test_v1/test_domains.py68
-rw-r--r--designate/tests/test_api/test_v2/__init__.py3
-rw-r--r--designate/tests/test_api/test_v2/test_zones.py297
-rw-r--r--designate/utils.py54
-rw-r--r--etc/designate/api-paste.ini14
21 files changed, 1050 insertions, 141 deletions
diff --git a/designate/api/__init__.py b/designate/api/__init__.py
index 9826a15f..2b19a7b9 100644
--- a/designate/api/__init__.py
+++ b/designate/api/__init__.py
@@ -24,6 +24,7 @@ cfg.CONF.register_group(cfg.OptGroup(
cfg.CONF.register_opts([
cfg.IntOpt('workers', default=None,
help='Number of worker processes to spawn'),
+ cfg.StrOpt('api-base-uri', default='http://127.0.0.1:9001/'),
cfg.StrOpt('api_host', default='0.0.0.0',
help='API Host'),
cfg.IntOpt('api_port', default=9001,
diff --git a/designate/api/middleware.py b/designate/api/middleware.py
index 8d13efde..5fefbd54 100644
--- a/designate/api/middleware.py
+++ b/designate/api/middleware.py
@@ -14,12 +14,16 @@
# License for the specific language governing permissions and limitations
# under the License.
import flask
+import webob.dec
from oslo.config import cfg
+from designate import exceptions
+from designate import wsgi
+from designate.context import DesignateContext
+from designate.openstack.common import jsonutils as json
from designate.openstack.common import local
from designate.openstack.common import log as logging
from designate.openstack.common import uuidutils
-from designate import wsgi
-from designate.context import DesignateContext
+from designate.openstack.common.rpc import common as rpc_common
LOG = logging.getLogger(__name__)
@@ -132,3 +136,70 @@ class NoAuthContextMiddleware(ContextMiddleware):
# Attach the context to the request environment
request.environ['context'] = context
+
+
+class FaultWrapperMiddleware(wsgi.Middleware):
+ def __init__(self, application):
+ super(FaultWrapperMiddleware, self).__init__(application)
+
+ LOG.info('Starting designate faultwrapper middleware')
+
+ @webob.dec.wsgify
+ def __call__(self, request):
+ try:
+ return request.get_response(self.application)
+ except exceptions.Base as e:
+ # Handle Designate Exceptions
+ status = e.error_code if hasattr(e, 'error_code') else 500
+
+ # Start building up a response
+ response = {
+ 'code': status
+ }
+
+ if e.error_type:
+ response['type'] = e.error_type
+
+ if e.error_message:
+ response['message'] = e.error_message
+
+ if e.errors:
+ response['errors'] = e.errors
+
+ return self._handle_exception(request, e, status, response)
+ except rpc_common.Timeout as e:
+ # Special case for RPC timeout's
+ response = {
+ 'code': 504,
+ 'type': 'timeout',
+ }
+
+ return self._handle_exception(request, e, 504, response)
+ except Exception as e:
+ # Handle all other exception types
+ return self._handle_exception(request, e)
+
+ def _handle_exception(self, request, e, status=500, response={}):
+ # Log the exception ASAP
+ LOG.exception(e)
+
+ headers = [
+ ('Content-Type', 'application/json'),
+ ]
+
+ # Set a response code and type, if they are missing.
+ if 'code' not in response:
+ response['code'] = status
+
+ if 'type' not in response:
+ response['type'] = 'unknown'
+
+ # Set the request ID, if we have one
+ if 'context' in request.environ:
+ response['request_id'] = request.environ['context'].request_id
+
+ # TODO(kiall): Send a fault notification
+
+ # Return the new response
+ return flask.Response(status=status, headers=headers,
+ response=json.dumps(response))
diff --git a/designate/api/v1/__init__.py b/designate/api/v1/__init__.py
index 451d90ac..31817fd2 100644
--- a/designate/api/v1/__init__.py
+++ b/designate/api/v1/__init__.py
@@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import flask
-import webob.dec
from stevedore import extension
from stevedore import named
from werkzeug import exceptions as wexceptions
@@ -22,12 +21,9 @@ from werkzeug import wrappers
from werkzeug.routing import BaseConverter
from werkzeug.routing import ValidationError
from oslo.config import cfg
-from designate.openstack.common import jsonutils as json
from designate.openstack.common import log as logging
from designate.openstack.common import uuidutils
-from designate.openstack.common.rpc import common as rpc_common
from designate import exceptions
-from designate import wsgi
LOG = logging.getLogger(__name__)
@@ -129,70 +125,3 @@ class UUIDConverter(BaseConverter):
def to_url(self, value):
return str(value)
-
-
-class FaultWrapperMiddleware(wsgi.Middleware):
- def __init__(self, application):
- super(FaultWrapperMiddleware, self).__init__(application)
-
- LOG.info('Starting designate faultwrapper middleware')
-
- @webob.dec.wsgify
- def __call__(self, request):
- try:
- return request.get_response(self.application)
- except exceptions.Base as e:
- # Handle Designate Exceptions
- status = e.error_code if hasattr(e, 'error_code') else 500
-
- # Start building up a response
- response = {
- 'code': status
- }
-
- if e.error_type:
- response['type'] = e.error_type
-
- if e.error_message:
- response['message'] = e.error_message
-
- if e.errors:
- response['errors'] = e.errors
-
- return self._handle_exception(request, e, status, response)
- except rpc_common.Timeout as e:
- # Special case for RPC timeout's
- response = {
- 'code': 504,
- 'type': 'timeout',
- }
-
- return self._handle_exception(request, e, 504, response)
- except Exception as e:
- # Handle all other exception types
- return self._handle_exception(request, e)
-
- def _handle_exception(self, request, e, status=500, response={}):
- # Log the exception ASAP
- LOG.exception(e)
-
- headers = [
- ('Content-Type', 'application/json'),
- ]
-
- # Set a response code and type, if they are missing.
- if 'code' not in response:
- response['code'] = status
-
- if 'type' not in response:
- response['type'] = 'unknown'
-
- # Set the request ID, if we have one
- if 'context' in request.environ:
- response['request_id'] = request.environ['context'].request_id
-
- # TODO(kiall): Send a fault notification
-
- # Return the new response
- return flask.Response(status=status, headers=headers,
- response=json.dumps(response))
diff --git a/designate/api/v2/__init__.py b/designate/api/v2/__init__.py
index 8b68ec24..8f3c0622 100644
--- a/designate/api/v2/__init__.py
+++ b/designate/api/v2/__init__.py
@@ -13,6 +13,7 @@
# 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 designate.api.v2 import patches # flake8: noqa
import pecan.deploy
from oslo.config import cfg
from designate.openstack.common import log as logging
diff --git a/designate/api/v2/controllers/recordsets.py b/designate/api/v2/controllers/recordsets.py
new file mode 100644
index 00000000..37918586
--- /dev/null
+++ b/designate/api/v2/controllers/recordsets.py
@@ -0,0 +1,49 @@
+# Copyright 2013 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.
+import pecan
+from designate.api.v2.controllers import rest
+from designate.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+
+
+class RecordSetsController(rest.RestController):
+ @pecan.expose(template='json:', content_type='application/json')
+ def get_one(self, zone_id, recordset_id):
+ """ Get RecordSet """
+ pass
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def get_all(self, zone_id):
+ """ List RecordSets """
+ pass
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def post_all(self, zone_id):
+ """ Create RecordSet """
+ pass
+
+ @pecan.expose(template='json:', content_type='application/json')
+ @pecan.expose(template='json:', content_type='application/json-patch+json')
+ def patch_one(self, zone_id, recordset_id):
+ """ Update RecordSet """
+ pass
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def delete_one(self, zone_id, recordset_id):
+ """ Delete RecordSet """
+ pass
diff --git a/designate/api/v2/controllers/rest.py b/designate/api/v2/controllers/rest.py
new file mode 100644
index 00000000..fb24754f
--- /dev/null
+++ b/designate/api/v2/controllers/rest.py
@@ -0,0 +1,130 @@
+# flake8: noqa
+# Copyright (c) <2011>, Jonathan LaCour
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import inspect
+import pecan
+import pecan.rest
+import pecan.routing
+from designate.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class RestController(pecan.rest.RestController):
+ """
+ Extension for Pecan's RestController to better handle POST/PUT/PATCH
+ requests.
+
+ Ideally, we get these additions merged upstream.
+ """
+
+ def _handle_post(self, method, remainder):
+ '''
+ Routes ``POST`` actions to the appropriate controller.
+ '''
+ # route to a post_all or get if no additional parts are available
+ if not remainder or remainder == ['']:
+ controller = self._find_controller('post_all', 'post')
+ if controller:
+ return controller, []
+ pecan.abort(404)
+
+ controller = getattr(self, remainder[0], None)
+ if controller and not inspect.ismethod(controller):
+ return pecan.routing.lookup_controller(controller, remainder[1:])
+
+ # finally, check for the regular post_one/post requests
+ controller = self._find_controller('post_one', 'post')
+ if controller:
+ return controller, remainder
+
+ pecan.abort(404)
+
+ def _handle_patch(self, method, remainder):
+ '''
+ Routes ``PATCH`` actions to the appropriate controller.
+ '''
+ # route to a patch_all or get if no additional parts are available
+ if not remainder or remainder == ['']:
+ controller = self._find_controller('patch_all', 'patch')
+ if controller:
+ return controller, []
+ pecan.abort(404)
+
+ controller = getattr(self, remainder[0], None)
+ if controller and not inspect.ismethod(controller):
+ return pecan.routing.lookup_controller(controller, remainder[1:])
+
+ # finally, check for the regular patch_one/patch requests
+ controller = self._find_controller('patch_one', 'patch')
+ if controller:
+ return controller, remainder
+
+ pecan.abort(404)
+
+ def _handle_put(self, method, remainder):
+ '''
+ Routes ``PUT`` actions to the appropriate controller.
+ '''
+ # route to a put_all or get if no additional parts are available
+ if not remainder or remainder == ['']:
+ controller = self._find_controller('put_all', 'put')
+ if controller:
+ return controller, []
+ pecan.abort(404)
+
+ controller = getattr(self, remainder[0], None)
+ if controller and not inspect.ismethod(controller):
+ return pecan.routing.lookup_controller(controller, remainder[1:])
+
+ # finally, check for the regular put_one/put requests
+ controller = self._find_controller('put_one', 'put')
+ if controller:
+ return controller, remainder
+
+ pecan.abort(404)
+
+ def _handle_delete(self, method, remainder):
+ '''
+ Routes ``DELETE`` actions to the appropriate controller.
+ '''
+ # route to a delete_all or get if no additional parts are available
+ if not remainder or remainder == ['']:
+ controller = self._find_controller('delete_all', 'delete')
+ if controller:
+ return controller, []
+ pecan.abort(404)
+
+ controller = getattr(self, remainder[0], None)
+ if controller and not inspect.ismethod(controller):
+ return pecan.routing.lookup_controller(controller, remainder[1:])
+
+ # finally, check for the regular delete_one/delete requests
+ controller = self._find_controller('delete_one', 'delete')
+ if controller:
+ return controller, remainder
+
+ pecan.abort(404)
diff --git a/designate/api/v2/controllers/root.py b/designate/api/v2/controllers/root.py
index 266090e1..03be5da5 100644
--- a/designate/api/v2/controllers/root.py
+++ b/designate/api/v2/controllers/root.py
@@ -14,14 +14,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.openstack.common import log as logging
-from designate.api.v2.controllers import schemas
from designate.api.v2.controllers import limits
+from designate.api.v2.controllers import schemas
+from designate.api.v2.controllers import zones
LOG = logging.getLogger(__name__)
class RootController(object):
- schemas = schemas.SchemasController()
+ """
+ This is /v2/ Controller. Pecan will find all controllers via the object
+ properties attached to this.
+ """
limits = limits.LimitsController()
- # zones = zones.ZonesController()
- # pools = pools.PoolsController()
+ schemas = schemas.SchemasController()
+ zones = zones.ZonesController()
diff --git a/designate/api/v2/controllers/zones.py b/designate/api/v2/controllers/zones.py
new file mode 100644
index 00000000..92288d43
--- /dev/null
+++ b/designate/api/v2/controllers/zones.py
@@ -0,0 +1,149 @@
+# Copyright 2013 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.
+import pecan
+from designate import utils
+from designate import schema
+from designate.api.v2.controllers import rest
+from designate.api.v2.controllers import recordsets
+from designate.api.v2.views import zones as zones_view
+from designate.central import rpcapi as central_rpcapi
+from designate.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+central_api = central_rpcapi.CentralAPI()
+
+
+class ZonesController(rest.RestController):
+ _view = zones_view.ZonesView()
+ _resource_schema = schema.Schema('v2', 'zone')
+ _collection_schema = schema.Schema('v2', 'zones')
+
+ recordsets = recordsets.RecordSetsController()
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def get_one(self, zone_id):
+ """ Get Zone """
+ request = pecan.request
+ context = request.environ['context']
+
+ # TODO(kiall): Validate we have a sane UUID for zone_id
+
+ zone = central_api.get_domain(context, zone_id)
+
+ return self._view.detail(context, request, zone)
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def get_all(self, **params):
+ """ List Zones """
+ request = pecan.request
+ context = request.environ['context']
+
+ # Extract the pagination params
+ #marker = params.pop('marker', None)
+ #limit = int(params.pop('limit', 30))
+
+ # Extract any filter params.
+ accepted_filters = ('name', 'email')
+ criterion = dict((k, params[k]) for k in accepted_filters
+ if k in params)
+
+ zones = central_api.find_domains(context, criterion)
+
+ return self._view.list(context, request, zones)
+
+ @pecan.expose(template='json:', content_type='application/json')
+ def post_all(self):
+ """ Create Zone """
+ request = pecan.request
+ response = pecan.response
+ context = request.environ['context']
+ body = request.body_dict
+
+ # Validate the request conforms to the schema
+ self._resource_schema.validate(body)
+
+ # Convert from APIv2 -> Central format
+ values = self._view.load(context, request, body)
+
+ # Create the zone
+ zone = central_api.create_domain(context, values)
+
+ # Prepare the response headers
+ response.status_int = 201
+ response.headers['Location'] = self._view._get_resource_href(request,
+ zone)
+
+ # Prepare and return the response body
+ return self._view.detail(context, request, zone)
+
+ @pecan.expose(template='json:', content_type='application/json')
+ @pecan.expose(template='json:', content_type='application/json-patch+json')
+ def patch_one(self, zone_id):
+ """ Update Zone """
+ # TODO(kiall): This needs cleanup to say the least..
+ request = pecan.request
+ context = request.environ['context']
+ body = request.body_dict
+
+ # TODO(kiall): Validate we have a sane UUID for zone_id
+
+ # Fetch the existing zone
+ zone = central_api.get_domain(context, zone_id)
+
+ # Convert to APIv2 Format
+ zone = self._view.detail(context, request, zone)
+
+ if request.content_type == 'application/json-patch+json':
+ # Possible pattern:
+ #
+ # 1) Load existing zone.
+ # 2) Apply patch, maintain list of changes.
+ # 3) Return changes, after passing through the code ^ for plain
+ # JSON.
+ #
+ # Difficulties:
+ #
+ # 1) "Nested" resources? records inside a recordset.
+ # 2) What to do when a zone doesn't exist in the first place?
+ # 3) ...?
+ raise NotImplemented('json-patch not implemented')
+ else:
+ zone = utils.deep_dict_merge(zone, body)
+
+ # Validate the request conforms to the schema
+ self._resource_schema.validate(zone)
+
+ values = self._view.load(context, request, body)
+ zone = central_api.update_domain(context, zone_id, values)
+
+ return self._view.detail(context, request, zone)
+
+ @pecan.expose(template=None, content_type='application/json')
+ def delete_one(self, zone_id):
+ """ Delete Zone """
+ request = pecan.request
+ response = pecan.response
+ context = request.environ['context']
+
+ # TODO(kiall): Validate we have a sane UUID for zone_id
+
+ central_api.delete_domain(context, zone_id)
+
+ response.status_int = 204
+
+ # NOTE: This is a hack and a half.. But Pecan needs it.
+ return ''
diff --git a/designate/api/v2/patches.py b/designate/api/v2/patches.py
new file mode 100644
index 00000000..1f0bf8ad
--- /dev/null
+++ b/designate/api/v2/patches.py
@@ -0,0 +1,38 @@
+# Copyright 2013 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.
+import pecan.core
+from designate.openstack.common import jsonutils
+
+JSON_TYPES = ('application/json', 'application/json-patch+json')
+
+
+class Request(pecan.core.Request):
+ @property
+ def body_dict(self):
+ """
+ Returns the body content as a dictonary, deserializing per the
+ Content-Type header.
+
+ We add this method to ease future XML support, so the main code
+ is not hardcoded to call pecans "request.json()" method.
+ """
+ if self.content_type in JSON_TYPES:
+ return jsonutils.load(self.body_file)
+ else:
+ raise Exception('TODO: Unsupported Content Type')
+
+
+pecan.core.Request = Request
diff --git a/designate/api/v2/views/__init__.py b/designate/api/v2/views/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/designate/api/v2/views/__init__.py
diff --git a/designate/api/v2/views/base.py b/designate/api/v2/views/base.py
new file mode 100644
index 00000000..5f2bc597
--- /dev/null
+++ b/designate/api/v2/views/base.py
@@ -0,0 +1,118 @@
+# Copyright 2013 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.
+import urllib
+from oslo.config import cfg
+from designate.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+CONF = cfg.CONF
+
+
+class BaseView(object):
+ """
+ The Views are responsible for coverting to/from the "intenal" and
+ "external" representations of collections and resources. This includes
+ adding "links" and adding/removing any other wrappers returned/received
+ as part of the API call.
+
+ For example, in the V2 API, we did s/domain/zone/. Adapting a record
+ resources "domain_id" <-> "zone_id" is the responsibility of a View.
+ """
+ _resource_name = None
+ _collection_name = None
+
+ def __init__(self):
+ super(BaseView, self).__init__()
+
+ self.base_uri = CONF['service:api']['api_base_uri'].rstrip('/')
+
+ def list(self, context, request, items):
+ """ View of a list of items """
+ result = {
+ "links": self._get_collection_links(request, items)
+ }
+
+ if 'detail' in request.GET and request.GET['detail'] == 'yes':
+ result[self._collection_name] = [self.detail(context, request, i)
+ for i in items]
+ else:
+ result[self._collection_name] = [self.basic(context, request, i)
+ for i in items]
+
+ return result
+
+ def basic(self, context, request, item):
+ """ Non-detailed view of a item """
+ return self.detail(context, request, item)
+
+ def _get_resource_links(self, request, item):
+ return {
+ "self": self._get_resource_href(request, item)
+ }
+
+ def _get_collection_links(self, request, items):
+ # TODO(kiall): Next and previous links should only be included
+ # when there are more/previous items.. This is what nova
+ # does.. But I think we can do better.
+
+ params = request.GET
+
+ result = {
+ "self": self._get_collection_href(request),
+ }
+
+ if 'marker' in params:
+ result['previous'] = self._get_previous_href(request, items)
+
+ if 'limit' in params and int(params['limit']) == len(items):
+ result['next'] = self._get_next_href(request, items)
+
+ return result
+
+ def _get_resource_href(self, request, item):
+ href = "%s/v2/%s/%s" % (self.base_uri, self._collection_name,
+ item['id'])
+
+ return href.rstrip('?')
+
+ def _get_collection_href(self, request):
+ params = request.GET
+
+ href = "%s/v2/%s?%s" % (self.base_uri, self._collection_name,
+ urllib.urlencode(params))
+
+ return href.rstrip('?')
+
+ def _get_next_href(self, request, items):
+ params = request.GET
+
+ # Add/Update the marker and sort_dir params
+ params['marker'] = items[-1]['id']
+ params.pop('sort_dir', None)
+
+ return "%s/v2/%s?%s" % (self.base_uri, self._collection_name,
+ urllib.urlencode(params))
+
+ def _get_previous_href(self, request, items):
+ params = request.GET
+
+ # Add/Update the marker and sort_dir params
+ params['marker'] = items[0]['id']
+ params['sort_dir'] = 'DESC'
+
+ return "%s/v2/%s?%s" % (self.base_uri, self._collection_name,
+ urllib.urlencode(params))
diff --git a/designate/api/v2/views/zones.py b/designate/api/v2/views/zones.py
new file mode 100644
index 00000000..71af59c8
--- /dev/null
+++ b/designate/api/v2/views/zones.py
@@ -0,0 +1,60 @@
+# Copyright 2013 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 designate.api.v2.views import base as base_view
+from designate.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+
+
+class ZonesView(base_view.BaseView):
+ """ Model a Zone API response as a python dictionary """
+
+ _resource_name = 'zone'
+ _collection_name = 'zones'
+
+ def detail(self, context, request, zone):
+ """ Detailed view of a zone """
+ # TODO(kiall): pool_id should not be hardcoded.. even temp :)
+ return {
+ "zone": {
+ "id": zone['id'],
+ "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
+ "project_id": zone['tenant_id'],
+ "name": zone['name'],
+ "email": zone['email'],
+ "description": zone['description'],
+ "ttl": zone['ttl'],
+ "serial": zone['serial'],
+ "status": "ACTIVE",
+ "version": zone['version'],
+ "created_at": zone['created_at'],
+ "updated_at": zone['updated_at'],
+ "links": self._get_resource_links(request, zone)
+ }
+ }
+
+ def load(self, context, request, body):
+ """ Extract a "central" compatible dict from an API call """
+ result = {}
+ item = body[self._resource_name]
+
+ # Copy keys which need no alterations
+ for k in ('id', 'name', 'email', 'description', 'ttl'):
+ if k in item:
+ result[k] = item[k]
+
+ return result
diff --git a/designate/resources/schemas/v2/recordset.json b/designate/resources/schemas/v2/recordset.json
index c60c1601..eeab0227 100644
--- a/designate/resources/schemas/v2/recordset.json
+++ b/designate/resources/schemas/v2/recordset.json
@@ -79,6 +79,17 @@
"description": "Date and time of last recordset modification",
"format": "date-time",
"readOnly": true
+ },
+ "links": {
+ "type": "object",
+ "additionalProperties": false,
+
+ "properties": {
+ "self": {
+ "type": "string",
+ "format": "url"
+ }
+ }
}
},
"oneOf": [
@@ -94,17 +105,6 @@
{"properties": {"type": {"enum": ["SSHFP"]}, "records": {"items": {"$ref": "rdata/sshfp#"}}}},
{"properties": {"type": {"enum": ["TXT"]}, "records": {"items": {"$ref": "rdata/txt#"}}}}
]
- },
- "links": {
- "type": "object",
- "additionalProperties": false,
-
- "properties": {
- "self": {
- "type": "string",
- "format": "url"
- }
- }
}
}
}
diff --git a/designate/resources/schemas/v2/zone.json b/designate/resources/schemas/v2/zone.json
index c1c3676d..85d87a11 100644
--- a/designate/resources/schemas/v2/zone.json
+++ b/designate/resources/schemas/v2/zone.json
@@ -29,7 +29,7 @@
"immutable": true
},
"project_id": {
- "type": "string",
+ "type": ["string", "null"],
"description": "Project identifier",
"maxLength": 36,
"immutable": true
@@ -92,17 +92,17 @@
"description": "Date and time of last zone modification",
"format": "date-time",
"readOnly": true
- }
- }
- },
- "links": {
- "type": "object",
- "additionalProperties": false,
+ },
+ "links": {
+ "type": "object",
+ "additionalProperties": false,
- "properties": {
- "self": {
- "type": "string",
- "format": "url"
+ "properties": {
+ "self": {
+ "type": "string",
+ "format": "url"
+ }
+ }
}
}
}
diff --git a/designate/tests/__init__.py b/designate/tests/__init__.py
index 701b67bf..ee4dd0ab 100644
--- a/designate/tests/__init__.py
+++ b/designate/tests/__init__.py
@@ -16,6 +16,7 @@
import copy
import unittest2
import mox
+import nose
from oslo.config import cfg
from designate.openstack.common import log as logging
from designate.openstack.common.notifier import test_notifier
@@ -130,6 +131,9 @@ class TestCase(unittest2.TestCase):
self.mox.UnsetStubs()
super(TestCase, self).tearDown()
+ def skip(self, message=None):
+ raise nose.SkipTest(message)
+
# Config Methods
def config(self, **kwargs):
group = kwargs.pop('group', None)
diff --git a/designate/tests/test_api/test_v1/__init__.py b/designate/tests/test_api/test_v1/__init__.py
index 993ebebc..b6ca653e 100644
--- a/designate/tests/test_api/test_v1/__init__.py
+++ b/designate/tests/test_api/test_v1/__init__.py
@@ -36,7 +36,8 @@ class ApiV1Test(ApiTestCase):
self.app = api_v1.factory({})
# Inject the FaultWrapper middleware
- self.app.wsgi_app = api_v1.FaultWrapperMiddleware(self.app.wsgi_app)
+ self.app.wsgi_app = middleware.FaultWrapperMiddleware(
+ self.app.wsgi_app)
# Inject the NoAuth middleware
self.app.wsgi_app = middleware.NoAuthContextMiddleware(
diff --git a/designate/tests/test_api/test_v1/test_domains.py b/designate/tests/test_api/test_v1/test_domains.py
index 5132ebbd..81a8dd76 100644
--- a/designate/tests/test_api/test_v1/test_domains.py
+++ b/designate/tests/test_api/test_v1/test_domains.py
@@ -114,40 +114,6 @@ class ApiV1DomainsTest(ApiV1Test):
#Create the domain, ensuring it fails with a 400
self.post('domains', data=fixture, status_code=400)
- def test_get_domains(self):
- response = self.get('domains')
-
- self.assertIn('domains', response.json)
- self.assertEqual(0, len(response.json['domains']))
-
- # Create a domain
- self.create_domain()
-
- response = self.get('domains')
-
- self.assertIn('domains', response.json)
- self.assertEqual(1, len(response.json['domains']))
-
- # Create a second domain
- self.create_domain(fixture=1)
-
- response = self.get('domains')
-
- self.assertIn('domains', response.json)
- self.assertEqual(2, len(response.json['domains']))
-
- @patch.object(central_service.Service, 'find_domains')
- def test_get_domains_trailing_slash(self, mock):
- self.get('domains/')
-
- # verify that the central service is called
- self.assertTrue(mock.called)
-
- @patch.object(central_service.Service, 'find_domains',
- side_effect=rpc_common.Timeout())
- def test_get_domains_timeout(self, _):
- self.get('domains', status_code=504)
-
def test_create_invalid_name(self):
# Prepare a domain
fixture = self.get_domain_fixture(0)
@@ -188,6 +154,40 @@ class ApiV1DomainsTest(ApiV1Test):
self.assertNotIn('id', response.json)
+ def test_get_domains(self):
+ response = self.get('domains')
+
+ self.assertIn('domains', response.json)
+ self.assertEqual(0, len(response.json['domains']))
+
+ # Create a domain
+ self.create_domain()
+
+ response = self.get('domains')
+
+ self.assertIn('domains', response.json)
+ self.assertEqual(1, len(response.json['domains']))
+
+ # Create a second domain
+ self.create_domain(fixture=1)
+
+ response = self.get('domains')
+
+ self.assertIn('domains', response.json)
+ self.assertEqual(2, len(response.json['domains']))
+
+ @patch.object(central_service.Service, 'find_domains')
+ def test_get_domains_trailing_slash(self, mock):
+ self.get('domains/')
+
+ # verify that the central service is called
+ self.assertTrue(mock.called)
+
+ @patch.object(central_service.Service, 'find_domains',
+ side_effect=rpc_common.Timeout())
+ def test_get_domains_timeout(self, _):
+ self.get('domains', status_code=504)
+
def test_get_domain(self):
# Create a domain
domain = self.create_domain()
diff --git a/designate/tests/test_api/test_v2/__init__.py b/designate/tests/test_api/test_v2/__init__.py
index f0f6b730..6dbdd9e5 100644
--- a/designate/tests/test_api/test_v2/__init__.py
+++ b/designate/tests/test_api/test_v2/__init__.py
@@ -35,6 +35,9 @@ class ApiV2TestCase(ApiTestCase):
# Create the application
self.app = api_v2.factory({})
+ # Inject the FaultWrapper middleware
+ self.app = middleware.FaultWrapperMiddleware(self.app)
+
# Inject the NoAuth middleware
self.app = middleware.NoAuthContextMiddleware(self.app)
diff --git a/designate/tests/test_api/test_v2/test_zones.py b/designate/tests/test_api/test_v2/test_zones.py
new file mode 100644
index 00000000..c9561891
--- /dev/null
+++ b/designate/tests/test_api/test_v2/test_zones.py
@@ -0,0 +1,297 @@
+# Copyright 2013 Hewlett-Packard Development Company, L.P.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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 mock import patch
+from designate import exceptions
+from designate.central import service as central_service
+from designate.openstack.common.rpc import common as rpc_common
+from designate.tests.test_api.test_v2 import ApiV2TestCase
+
+
+class ApiV2ZonesTest(ApiV2TestCase):
+ __test__ = True
+
+ def setUp(self):
+ super(ApiV2ZonesTest, self).setUp()
+
+ # Create a server
+ self.create_server()
+
+ def test_create_zone(self):
+ # Create a zone
+ fixture = self.get_domain_fixture(0)
+ response = self.client.post_json('/zones/', {'zone': fixture})
+
+ # Check the headers are what we expect
+ self.assertEqual(201, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ # Check the body structure is what we expect
+ self.assertIn('zone', response.json)
+ self.assertIn('links', response.json['zone'])
+ self.assertIn('self', response.json['zone']['links'])
+
+ # Check the values returned are what we expect
+ self.assertIn('id', response.json['zone'])
+ self.assertIn('created_at', response.json['zone'])
+ self.assertEqual('ACTIVE', response.json['zone']['status'])
+ self.assertIsNone(response.json['zone']['updated_at'])
+
+ for k in fixture:
+ self.assertEqual(fixture[k], response.json['zone'][k])
+
+ def test_create_zone_validation(self):
+ # NOTE: The schemas should be tested separatly to the API. So we
+ # don't need to test every variation via the API itself.
+ # Fetch a fixture
+ fixture = self.get_domain_fixture(0)
+
+ # Add a junk field to the wrapper
+ body = {'zone': fixture, 'junk': 'Junk Field'}
+
+ # Ensure it fails with a 400
+ response = self.client.post_json('/zones/', body, status=400)
+ self.assertEqual(400, response.status_int)
+
+ # Add a junk field to the body
+ fixture['junk'] = 'Junk Field'
+
+ # Ensure it fails with a 400
+ body = {'zone': fixture}
+ self.client.post_json('/zones/', body, status=400)
+
+ @patch.object(central_service.Service, 'create_domain',
+ side_effect=rpc_common.Timeout())
+ def test_create_zone_timeout(self, _):
+ fixture = self.get_domain_fixture(0)
+
+ body = {'zone': fixture}
+ self.client.post_json('/zones/', body, status=504)
+
+ @patch.object(central_service.Service, 'create_domain',
+ side_effect=exceptions.DuplicateDomain())
+ def test_create_zone_duplicate(self, _):
+ fixture = self.get_domain_fixture(0)
+
+ body = {'zone': fixture}
+ self.client.post_json('/zones/', body, status=409)
+
+ def test_get_zones(self):
+ response = self.client.get('/zones/')
+
+ # Check the headers are what we expect
+ self.assertEqual(200, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ # Check the body structure is what we expect
+ self.assertIn('zones', response.json)
+ self.assertIn('links', response.json)
+ self.assertIn('self', response.json['links'])
+
+ # We should start with 0 zones
+ self.assertEqual(0, len(response.json['zones']))
+
+ # Test with 1 zone
+ self.create_domain()
+
+ response = self.client.get('/zones/')
+
+ self.assertIn('zones', response.json)
+ self.assertEqual(1, len(response.json['zones']))
+
+ # test with 2 zones
+ self.create_domain(fixture=1)
+
+ response = self.client.get('/zones/')
+
+ self.assertIn('zones', response.json)
+ self.assertEqual(2, len(response.json['zones']))
+
+ @patch.object(central_service.Service, 'find_domains',
+ side_effect=rpc_common.Timeout())
+ def test_get_zones_timeout(self, _):
+ self.client.get('/zones/', status=504)
+
+ def test_get_zone(self):
+ # Create a zone
+ zone = self.create_domain()
+
+ response = self.client.get('/zones/%s' % zone['id'])
+
+ # Check the headers are what we expect
+ self.assertEqual(200, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ # Check the body structure is what we expect
+ self.assertIn('zone', response.json)
+ self.assertIn('links', response.json['zone'])
+ self.assertIn('self', response.json['zone']['links'])
+
+ # Check the values returned are what we expect
+ self.assertIn('id', response.json['zone'])
+ self.assertIn('created_at', response.json['zone'])
+ self.assertEqual('ACTIVE', response.json['zone']['status'])
+ self.assertIsNone(response.json['zone']['updated_at'])
+ self.assertEqual(zone['name'], response.json['zone']['name'])
+ self.assertEqual(zone['email'], response.json['zone']['email'])
+
+ @patch.object(central_service.Service, 'get_domain',
+ side_effect=rpc_common.Timeout())
+ def test_get_zone_timeout(self, _):
+ self.client.get('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ status=504)
+
+ @patch.object(central_service.Service, 'get_domain',
+ side_effect=exceptions.DomainNotFound())
+ def test_get_zone_missing(self, _):
+ self.client.get('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ status=404)
+
+ def test_get_zone_invalid_id(self):
+ self.skip('We don\'t guard against this in APIv2 yet')
+
+ # The letter "G" is not valid in a UUID
+ self.client.get('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff9GG',
+ status=404)
+
+ # Badly formed UUID
+ self.client.get('/zones/2fdadfb1cf964259ac6bbb7b6d2ff9GG', status=404)
+
+ # Integer
+ self.client.get('/zones/12345', status=404)
+
+ def test_update_zone(self):
+ # Create a zone
+ zone = self.create_domain()
+
+ # Prepare an update body
+ body = {'zone': {'email': 'prefix-%s' % zone['email']}}
+
+ response = self.client.patch_json('/zones/%s' % zone['id'], body,
+ status=200)
+
+ # Check the headers are what we expect
+ self.assertEqual(200, response.status_int)
+ self.assertEqual('application/json', response.content_type)
+
+ # Check the body structure is what we expect
+ self.assertIn('zone', response.json)
+ self.assertIn('links', response.json['zone'])
+ self.assertIn('self', response.json['zone']['links'])
+
+ # Check the values returned are what we expect
+ self.assertIn('id', response.json['zone'])
+ self.assertIsNotNone(response.json['zone']['updated_at'])
+ self.assertEqual('prefix-%s' % zone['email'],
+ response.json['zone']['email'])
+
+ def test_update_zone_validation(self):
+ # NOTE: The schemas should be tested separatly to the API. So we
+ # don't need to test every variation via the API itself.
+ # Create a zone
+ zone = self.create_domain()
+
+ # Prepare an update body with junk in the wrapper
+ body = {'zone': {'email': 'prefix-%s' % zone['email']},
+ 'junk': 'Junk Field'}
+
+ # Ensure it fails with a 400
+ self.client.patch_json('/zones/%s' % zone['id'], body, status=400)
+
+ # Prepare an update body with junk in the body
+ body = {'zone': {'email': 'prefix-%s' % zone['email'],
+ 'junk': 'Junk Field'}}
+
+ # Ensure it fails with a 400
+ self.client.patch_json('/zones/%s' % zone['id'], body, status=400)
+
+ @patch.object(central_service.Service, 'get_domain',
+ side_effect=exceptions.DuplicateDomain())
+ def test_update_zone_duplicate(self, _):
+ # Prepare an update body
+ body = {'zone': {'email': 'example@example.org'}}
+
+ # Ensure it fails with a 409
+ self.client.patch_json('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ body, status=409)
+
+ @patch.object(central_service.Service, 'get_domain',
+ side_effect=rpc_common.Timeout())
+ def test_update_zone_timeout(self, _):
+ # Prepare an update body
+ body = {'zone': {'email': 'example@example.org'}}
+
+ # Ensure it fails with a 504
+ self.client.patch_json('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ body, status=504)
+
+ @patch.object(central_service.Service, 'get_domain',
+ side_effect=exceptions.DomainNotFound())
+ def test_update_zone_missing(self, _):
+ # Prepare an update body
+ body = {'zone': {'email': 'example@example.org'}}
+
+ # Ensure it fails with a 404
+ self.client.patch_json('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ body, status=404)
+
+ def test_update_zone_invalid_id(self):
+ self.skip('We don\'t guard against this in APIv2 yet')
+
+ # Prepare an update body
+ body = {'zone': {'email': 'example@example.org'}}
+
+ # The letter "G" is not valid in a UUID
+ self.client.patch_json('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff9GG',
+ body, status=404)
+
+ # Badly formed UUID
+ self.client.patch_json('/zones/2fdadfb1cf964259ac6bbb7b6d2ff980',
+ body, status=404)
+
+ # Integer
+ self.client.patch_json('/zones/12345',
+ body, status=404)
+
+ def test_delete_zone(self):
+ zone = self.create_domain()
+
+ self.client.delete('/zones/%s' % zone['id'], status=204)
+
+ @patch.object(central_service.Service, 'delete_domain',
+ side_effect=rpc_common.Timeout())
+ def test_delete_zone_timeout(self, _):
+ self.client.delete('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ status=504)
+
+ @patch.object(central_service.Service, 'delete_domain',
+ side_effect=exceptions.DomainNotFound())
+ def test_delete_zone_missing(self, _):
+ self.client.delete('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980',
+ status=404)
+
+ def test_delete_zone_invalid_id(self):
+ self.skip('We don\'t guard against this in APIv2 yet')
+
+ # The letter "G" is not valid in a UUID
+ self.client.delete('/zones/2fdadfb1-cf96-4259-ac6b-bb7b6d2ff9GG',
+ status=404)
+
+ # Badly formed UUID
+ self.client.delete('/zones/2fdadfb1cf964259ac6bbb7b6d2ff980',
+ status=404)
+
+ # Integer
+ self.client.delete('/zones/12345', status=404)
diff --git a/designate/utils.py b/designate/utils.py
index 794461d9..95f6d9bd 100644
--- a/designate/utils.py
+++ b/designate/utils.py
@@ -13,6 +13,7 @@
# 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 copy
import os
import pkg_resources
import json
@@ -183,3 +184,56 @@ def increment_serial(serial=0):
new_serial = serial + 1
return new_serial
+
+
+def quote_string(string):
+ inparts = string.split(' ')
+ outparts = []
+ tmp = None
+
+ for part in inparts:
+ if part == '':
+ continue
+ elif part[0] == '"' and part[-1:] == '"' and part[-2:] != '\\"':
+ # Handle Quoted Words
+ outparts.append(part.strip('"'))
+ elif part[0] == '"':
+ # Handle Start of Quoted Sentance
+ tmp = part[1:]
+ elif tmp is not None and part[-1:] == '"' and part[-2:] != '\\"':
+ # Handle End of Quoted Sentance
+ tmp += " " + part.strip('"')
+ outparts.append(tmp)
+ tmp = None
+ elif tmp is not None:
+ # Handle Middle of Quoted Sentance
+ tmp += " " + part
+ else:
+ # Handle Standalone words
+ outparts.append(part)
+
+ if tmp is not None:
+ # Handle unclosed quoted strings
+ outparts.append(tmp)
+
+ # This looks odd, but both calls are necessary to ensure the end results
+ # is always consistent.
+ outparts = [o.replace('\\"', '"') for o in outparts]
+ outparts = [o.replace('"', '\\"') for o in outparts]
+
+ return '"' + '" "'.join(outparts) + '"'
+
+
+def deep_dict_merge(a, b):
+ if not isinstance(b, dict):
+ return b
+
+ result = copy.deepcopy(a)
+
+ for k, v in b.iteritems():
+ if k in result and isinstance(result[k], dict):
+ result[k] = deep_dict_merge(result[k], v)
+ else:
+ result[k] = copy.deepcopy(v)
+
+ return result
diff --git a/etc/designate/api-paste.ini b/etc/designate/api-paste.ini
index d929bc82..24a4305a 100644
--- a/etc/designate/api-paste.ini
+++ b/etc/designate/api-paste.ini
@@ -9,19 +9,16 @@ paste.app_factory = designate.api.versions:factory
[composite:osapi_dns_v1]
use = call:designate.api.middleware:auth_pipeline_factory
-noauth = noauthcontext maintenance faultwrapper_v1 osapi_dns_app_v1
-keystone = authtoken keystonecontext maintenance faultwrapper_v1 osapi_dns_app_v1
+noauth = noauthcontext maintenance faultwrapper osapi_dns_app_v1
+keystone = authtoken keystonecontext maintenance faultwrapper osapi_dns_app_v1
[app:osapi_dns_app_v1]
paste.app_factory = designate.api.v1:factory
-[filter:faultwrapper_v1]
-paste.filter_factory = designate.api.v1:FaultWrapperMiddleware.factory
-
[composite:osapi_dns_v2]
use = call:designate.api.middleware:auth_pipeline_factory
-noauth = noauthcontext maintenance osapi_dns_app_v2
-keystone = authtoken keystonecontext maintenance osapi_dns_app_v2
+noauth = noauthcontext maintenance faultwrapper osapi_dns_app_v2
+keystone = authtoken keystonecontext maintenance faultwrapper osapi_dns_app_v2
[app:osapi_dns_app_v2]
paste.app_factory = designate.api.v2:factory
@@ -35,5 +32,8 @@ paste.filter_factory = designate.api.middleware:NoAuthContextMiddleware.factory
[filter:keystonecontext]
paste.filter_factory = designate.api.middleware:KeystoneContextMiddleware.factory
+[filter:faultwrapper]
+paste.filter_factory = designate.api.middleware:FaultWrapperMiddleware.factory
+
[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory