Add APIv2 Zones Controller
Change-Id: Ia85d22e4766cbf4e2af630d12a8b0ca3fc00ab1c
This commit is contained in:
parent
ad80f3f6ea
commit
24ca94cda8
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
49
designate/api/v2/controllers/recordsets.py
Normal file
49
designate/api/v2/controllers/recordsets.py
Normal file
@ -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
|
130
designate/api/v2/controllers/rest.py
Normal file
130
designate/api/v2/controllers/rest.py
Normal file
@ -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)
|
@ -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()
|
||||
|
149
designate/api/v2/controllers/zones.py
Normal file
149
designate/api/v2/controllers/zones.py
Normal file
@ -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 ''
|
38
designate/api/v2/patches.py
Normal file
38
designate/api/v2/patches.py
Normal file
@ -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
|
0
designate/api/v2/views/__init__.py
Normal file
0
designate/api/v2/views/__init__.py
Normal file
118
designate/api/v2/views/base.py
Normal file
118
designate/api/v2/views/base.py
Normal file
@ -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))
|
60
designate/api/v2/views/zones.py
Normal file
60
designate/api/v2/views/zones.py
Normal file
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
297
designate/tests/test_api/test_v2/test_zones.py
Normal file
297
designate/tests/test_api/test_v2/test_zones.py
Normal file
@ -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)
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user