Merge "Expose /v2/recordsets api endpoint"
This commit is contained in:
commit
4ea5e1dc1e
47
designate/api/v2/controllers/common.py
Normal file
47
designate/api/v2/controllers/common.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Copyright 2016 Rackspace
|
||||
#
|
||||
# Author: James Li <james.li@rackspace.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 import utils
|
||||
|
||||
|
||||
def retrieve_matched_rrsets(context, controller_obj, zone_id, **params):
|
||||
if zone_id:
|
||||
# NOTE: We need to ensure the zone actually exists, otherwise we may
|
||||
# return deleted recordsets instead of a zone not found
|
||||
controller_obj.central_api.get_zone(context, zone_id)
|
||||
|
||||
# Extract the pagination params
|
||||
marker, limit, sort_key, sort_dir = utils.get_paging_params(
|
||||
params, controller_obj.SORT_KEYS)
|
||||
|
||||
# Extract any filter params.
|
||||
accepted_filters = (
|
||||
'name', 'type', 'ttl', 'data', 'status', 'description', )
|
||||
criterion = controller_obj._apply_filter_params(
|
||||
params, accepted_filters, {})
|
||||
|
||||
if zone_id:
|
||||
criterion['zone_id'] = zone_id
|
||||
|
||||
recordsets = controller_obj.central_api.find_recordsets(
|
||||
context, criterion, marker, limit, sort_key, sort_dir)
|
||||
|
||||
return recordsets
|
||||
|
||||
|
||||
def get_rrset_canonical_location(request, zone_id, rrset_id):
|
||||
return '{base_url}/v2/zones/{zone_id}/recordsets/{id}'.format(
|
||||
base_url=request.host_url, zone_id=zone_id,
|
||||
id=rrset_id)
|
@ -16,170 +16,43 @@
|
||||
import pecan
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
from designate.api.v2.controllers import common
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.objects import RecordSet
|
||||
from designate.objects.adapters import DesignateAdapter
|
||||
from designate.i18n import _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecordSetsController(rest.RestController):
|
||||
class RecordSetsViewController(rest.RestController):
|
||||
SORT_KEYS = ['created_at', 'id', 'updated_at', 'zone_id', 'tenant_id',
|
||||
'name', 'type', 'ttl', 'records']
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def get_one(self, zone_id, recordset_id):
|
||||
@utils.validate_uuid('recordset_id')
|
||||
def get_one(self, recordset_id):
|
||||
"""Get RecordSet"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
|
||||
recordset = self.central_api.get_recordset(context, zone_id,
|
||||
recordset_id)
|
||||
rrset = self.central_api.get_recordset(context, None, recordset_id)
|
||||
|
||||
LOG.info(_LI("Retrieved %(recordset)s"), {'recordset': recordset})
|
||||
LOG.info(_LI("Retrieved %(recordset)s"), {'recordset': rrset})
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordset, request=request)
|
||||
canonical_loc = common.get_rrset_canonical_location(request,
|
||||
rrset.zone_id,
|
||||
recordset_id)
|
||||
pecan.core.redirect(location=canonical_loc, code=301)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def get_all(self, zone_id, **params):
|
||||
def get_all(self, **params):
|
||||
"""List RecordSets"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
|
||||
# NOTE: We need to ensure the zone actually exists, otherwise we may
|
||||
# return deleted recordsets instead of a zone not found
|
||||
self.central_api.get_zone(context, zone_id)
|
||||
|
||||
# Extract the pagination params
|
||||
marker, limit, sort_key, sort_dir = utils.get_paging_params(
|
||||
params, self.SORT_KEYS)
|
||||
|
||||
# Extract any filter params.
|
||||
accepted_filters = (
|
||||
'name', 'type', 'ttl', 'data', 'status', 'description', )
|
||||
criterion = self._apply_filter_params(
|
||||
params, accepted_filters, {})
|
||||
|
||||
criterion['zone_id'] = zone_id
|
||||
|
||||
recordsets = self.central_api.find_recordsets(
|
||||
context, criterion, marker, limit, sort_key, sort_dir)
|
||||
recordsets = common.retrieve_matched_rrsets(context, self, None,
|
||||
**params)
|
||||
|
||||
LOG.info(_LI("Retrieved %(recordsets)s"), {'recordsets': recordsets})
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordsets, request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def post_all(self, zone_id):
|
||||
"""Create RecordSet"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
body = request.body_dict
|
||||
|
||||
recordset = DesignateAdapter.parse('API_v2', body, RecordSet())
|
||||
|
||||
recordset.validate()
|
||||
|
||||
# SOA recordsets cannot be created manually
|
||||
if recordset.type == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
"Creating a SOA recordset is not allowed")
|
||||
|
||||
# Create the recordset
|
||||
recordset = self.central_api.create_recordset(
|
||||
context, zone_id, recordset)
|
||||
|
||||
# Prepare the response headers
|
||||
if recordset['status'] == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 201
|
||||
|
||||
LOG.info(_LI("Created %(recordset)s"), {'recordset': recordset})
|
||||
|
||||
recordset = DesignateAdapter.render(
|
||||
'API_v2', recordset, request=request)
|
||||
|
||||
response.headers['Location'] = recordset['links']['self']
|
||||
|
||||
# Prepare and return the response body
|
||||
return recordset
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def put_one(self, zone_id, recordset_id):
|
||||
"""Update RecordSet"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
body = request.body_dict
|
||||
response = pecan.response
|
||||
|
||||
# Fetch the existing recordset
|
||||
recordset = self.central_api.get_recordset(context, zone_id,
|
||||
recordset_id)
|
||||
|
||||
# TODO(graham): Move this further down the stack
|
||||
if recordset.managed and not context.edit_managed_records:
|
||||
raise exceptions.BadRequest('Managed records may not be updated')
|
||||
|
||||
# SOA recordsets cannot be updated manually
|
||||
if recordset['type'] == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
'Updating SOA recordsets is not allowed')
|
||||
|
||||
# NS recordsets at the zone root cannot be manually updated
|
||||
if recordset['type'] == 'NS':
|
||||
zone = self.central_api.get_zone(context, zone_id)
|
||||
if recordset['name'] == zone['name']:
|
||||
raise exceptions.BadRequest(
|
||||
'Updating a root zone NS record is not allowed')
|
||||
|
||||
# Convert to APIv2 Format
|
||||
|
||||
recordset = DesignateAdapter.parse('API_v2', body, recordset)
|
||||
|
||||
recordset.validate()
|
||||
|
||||
# Persist the resource
|
||||
recordset = self.central_api.update_recordset(context, recordset)
|
||||
|
||||
LOG.info(_LI("Updated %(recordset)s"), {'recordset': recordset})
|
||||
|
||||
if recordset['status'] == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 200
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordset, request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def delete_one(self, zone_id, recordset_id):
|
||||
"""Delete RecordSet"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
# Fetch the existing recordset
|
||||
recordset = self.central_api.get_recordset(context, zone_id,
|
||||
recordset_id)
|
||||
if recordset['type'] == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
'Deleting a SOA recordset is not allowed')
|
||||
|
||||
recordset = self.central_api.delete_recordset(
|
||||
context, zone_id, recordset_id)
|
||||
|
||||
LOG.info(_LI("Deleted %(recordset)s"), {'recordset': recordset})
|
||||
|
||||
response.status_int = 202
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordset, request=request)
|
||||
|
@ -26,6 +26,7 @@ from designate.api.v2.controllers import pools
|
||||
from designate.api.v2.controllers import service_status
|
||||
from designate.api.v2.controllers import zones
|
||||
from designate.api.v2.controllers import tsigkeys
|
||||
from designate.api.v2.controllers import recordsets
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -60,3 +61,4 @@ class RootController(object):
|
||||
pools = pools.PoolsController()
|
||||
service_statuses = service_status.ServiceStatusController()
|
||||
tsigkeys = tsigkeys.TsigKeysController()
|
||||
recordsets = recordsets.RecordSetsViewController()
|
||||
|
@ -20,7 +20,7 @@ from oslo_log import log as logging
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.api.v2.controllers import recordsets
|
||||
from designate.api.v2.controllers.zones import recordsets
|
||||
from designate.api.v2.controllers.zones import tasks
|
||||
from designate.api.v2.controllers.zones import nameservers
|
||||
from designate import objects
|
||||
|
158
designate/api/v2/controllers/zones/recordsets.py
Normal file
158
designate/api/v2/controllers/zones/recordsets.py
Normal file
@ -0,0 +1,158 @@
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@hpe.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 oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
from designate.api.v2.controllers import common
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.objects import RecordSet
|
||||
from designate.objects.adapters import DesignateAdapter
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecordSetsController(rest.RestController):
|
||||
SORT_KEYS = ['created_at', 'id', 'updated_at', 'zone_id', 'tenant_id',
|
||||
'name', 'type', 'ttl', 'records']
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def get_one(self, zone_id, recordset_id):
|
||||
"""Get RecordSet"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
|
||||
return DesignateAdapter.render(
|
||||
'API_v2',
|
||||
self.central_api.get_recordset(
|
||||
context, zone_id, recordset_id),
|
||||
request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def get_all(self, zone_id, **params):
|
||||
"""List RecordSets"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
recordsets = common.retrieve_matched_rrsets(context, self, zone_id,
|
||||
**params)
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordsets, request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def post_all(self, zone_id):
|
||||
"""Create RecordSet"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
body = request.body_dict
|
||||
|
||||
recordset = DesignateAdapter.parse('API_v2', body, RecordSet())
|
||||
|
||||
recordset.validate()
|
||||
|
||||
# SOA recordsets cannot be created manually
|
||||
if recordset.type == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
"Creating a SOA recordset is not allowed")
|
||||
|
||||
# Create the recordset
|
||||
recordset = self.central_api.create_recordset(
|
||||
context, zone_id, recordset)
|
||||
|
||||
# Prepare the response headers
|
||||
if recordset['status'] == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 201
|
||||
|
||||
recordset = DesignateAdapter.render(
|
||||
'API_v2', recordset, request=request)
|
||||
|
||||
response.headers['Location'] = recordset['links']['self']
|
||||
|
||||
# Prepare and return the response body
|
||||
return recordset
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def put_one(self, zone_id, recordset_id):
|
||||
"""Update RecordSet"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
body = request.body_dict
|
||||
response = pecan.response
|
||||
|
||||
# Fetch the existing recordset
|
||||
recordset = self.central_api.get_recordset(context, zone_id,
|
||||
recordset_id)
|
||||
|
||||
# TODO(graham): Move this further down the stack
|
||||
if recordset.managed and not context.edit_managed_records:
|
||||
raise exceptions.BadRequest('Managed records may not be updated')
|
||||
|
||||
# SOA recordsets cannot be updated manually
|
||||
if recordset['type'] == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
'Updating SOA recordsets is not allowed')
|
||||
|
||||
# NS recordsets at the zone root cannot be manually updated
|
||||
if recordset['type'] == 'NS':
|
||||
zone = self.central_api.get_zone(context, zone_id)
|
||||
if recordset['name'] == zone['name']:
|
||||
raise exceptions.BadRequest(
|
||||
'Updating a root zone NS record is not allowed')
|
||||
|
||||
# Convert to APIv2 Format
|
||||
|
||||
recordset = DesignateAdapter.parse('API_v2', body, recordset)
|
||||
|
||||
recordset.validate()
|
||||
|
||||
# Persist the resource
|
||||
recordset = self.central_api.update_recordset(context, recordset)
|
||||
|
||||
if recordset['status'] == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 200
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordset, request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id', 'recordset_id')
|
||||
def delete_one(self, zone_id, recordset_id):
|
||||
"""Delete RecordSet"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
# Fetch the existing recordset
|
||||
recordset = self.central_api.get_recordset(context, zone_id,
|
||||
recordset_id)
|
||||
if recordset['type'] == 'SOA':
|
||||
raise exceptions.BadRequest(
|
||||
'Deleting a SOA recordset is not allowed')
|
||||
|
||||
recordset = self.central_api.delete_recordset(
|
||||
context, zone_id, recordset_id)
|
||||
response.status_int = 202
|
||||
|
||||
return DesignateAdapter.render('API_v2', recordset, request=request)
|
@ -1243,6 +1243,9 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
self.pool_manager_api.update_zone(context, zone)
|
||||
|
||||
recordset.zone_name = zone.name
|
||||
recordset.obj_reset_changes(['zone_name'])
|
||||
|
||||
return recordset
|
||||
|
||||
def _validate_recordset(self, context, zone, recordset):
|
||||
@ -1306,15 +1309,18 @@ class Service(service.RPCService, service.Service):
|
||||
return (recordset, zone)
|
||||
|
||||
def get_recordset(self, context, zone_id, recordset_id):
|
||||
zone = self.storage.get_zone(context, zone_id)
|
||||
recordset = self.storage.get_recordset(context, recordset_id)
|
||||
|
||||
# Ensure the zone_id matches the record's zone_id
|
||||
if zone.id != recordset.zone_id:
|
||||
raise exceptions.RecordSetNotFound()
|
||||
if zone_id:
|
||||
zone = self.storage.get_zone(context, zone_id)
|
||||
# Ensure the zone_id matches the record's zone_id
|
||||
if zone.id != recordset.zone_id:
|
||||
raise exceptions.RecordSetNotFound()
|
||||
else:
|
||||
zone = self.storage.get_zone(context, recordset.zone_id)
|
||||
|
||||
target = {
|
||||
'zone_id': zone_id,
|
||||
'zone_id': zone.id,
|
||||
'zone_name': zone.name,
|
||||
'recordset_id': recordset.id,
|
||||
'tenant_id': zone.tenant_id,
|
||||
@ -1322,6 +1328,8 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
policy.check('get_recordset', context, target)
|
||||
|
||||
recordset.zone_name = zone.name
|
||||
recordset.obj_reset_changes(['zone_name'])
|
||||
recordset = recordset
|
||||
|
||||
return recordset
|
||||
@ -1367,7 +1375,7 @@ class Service(service.RPCService, service.Service):
|
||||
raise exceptions.BadRequest('Moving a recordset between tenants '
|
||||
'is not allowed')
|
||||
|
||||
if 'zone_id' in changes:
|
||||
if 'zone_id' in changes or 'zone_name' in changes:
|
||||
raise exceptions.BadRequest('Moving a recordset between zones '
|
||||
'is not allowed')
|
||||
|
||||
@ -1460,6 +1468,9 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
self.pool_manager_api.update_zone(context, zone)
|
||||
|
||||
recordset.zone_name = zone.name
|
||||
recordset.obj_reset_changes(['zone_name'])
|
||||
|
||||
return recordset
|
||||
|
||||
@transaction
|
||||
|
@ -78,7 +78,7 @@ class APIv2Adapter(base.DesignateAdapter):
|
||||
#####################
|
||||
|
||||
@classmethod
|
||||
def _get_resource_links(cls, object, request):
|
||||
def _get_resource_links(cls, obj, request):
|
||||
if cfg.CONF['service:api'].enable_host_header:
|
||||
try:
|
||||
base_uri = request.host_url
|
||||
@ -87,11 +87,11 @@ class APIv2Adapter(base.DesignateAdapter):
|
||||
else:
|
||||
base_uri = cls.BASE_URI
|
||||
|
||||
return {'self': '%s%s/%s' %
|
||||
(base_uri, cls._get_path(request), object.id)}
|
||||
path = cls._get_path(request, obj)
|
||||
return {'self': '%s%s/%s' % (base_uri, path, obj.id)}
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request):
|
||||
def _get_path(cls, request, *args):
|
||||
path = request.path.lstrip('/').split('/')
|
||||
item_path = ''
|
||||
for part in path:
|
||||
|
@ -32,6 +32,9 @@ class RecordSetAPIv2Adapter(base.APIv2Adapter):
|
||||
"name": {
|
||||
'immutable': True
|
||||
},
|
||||
"zone_name": {
|
||||
'read_only': True,
|
||||
},
|
||||
"type": {
|
||||
'rename': 'type',
|
||||
'immutable': True
|
||||
@ -112,6 +115,27 @@ class RecordSetAPIv2Adapter(base.APIv2Adapter):
|
||||
return super(RecordSetAPIv2Adapter, cls)._parse_object(
|
||||
new_recordset, recordset, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request, obj):
|
||||
ori_path = request.path
|
||||
path = ori_path.lstrip('/').split('/')
|
||||
insert_zones = False
|
||||
to_insert = ''
|
||||
if 'zones' not in path and obj is not None:
|
||||
insert_zones = True
|
||||
to_insert = 'zones/{0}'.format(obj.zone_id)
|
||||
|
||||
item_path = ''
|
||||
for part in path:
|
||||
if part == cls.MODIFICATIONS['options']['collection_name']:
|
||||
item_path += '/' + part
|
||||
return item_path
|
||||
elif insert_zones and to_insert and part == 'v2':
|
||||
item_path += '/v2/{0}'.format(to_insert)
|
||||
insert_zones = False # make sure only insert once if needed
|
||||
else:
|
||||
item_path += '/' + part
|
||||
|
||||
|
||||
class RecordSetListAPIv2Adapter(base.APIv2Adapter):
|
||||
|
||||
|
@ -48,7 +48,7 @@ class ZoneExportAPIv2Adapter(base.APIv2Adapter):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request):
|
||||
def _get_path(cls, request, *args):
|
||||
return '/v2/zones/tasks/exports'
|
||||
|
||||
@classmethod
|
||||
|
@ -84,7 +84,7 @@ class ZoneTransferRequestAPIv2Adapter(base.APIv2Adapter):
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request):
|
||||
def _get_path(cls, request, *args):
|
||||
return '/v2/zones/tasks/transfer_requests'
|
||||
|
||||
|
||||
|
@ -94,10 +94,19 @@ class RecordSet(base.DictObjectMixin, base.PersistentObjectMixin,
|
||||
'format': 'uuid'
|
||||
},
|
||||
},
|
||||
'name': {
|
||||
'zone_name': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'description': 'Zone name',
|
||||
'format': 'domainname',
|
||||
'maxLength': 255,
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'name': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'description': 'Recordset name',
|
||||
'format': 'hostname',
|
||||
'maxLength': 255,
|
||||
},
|
||||
|
@ -287,7 +287,9 @@ class SQLAlchemy(object):
|
||||
records_table,
|
||||
recordsets_table.c.id == records_table.c.recordset_id)
|
||||
|
||||
inner_q = select([recordsets_table.c.id]).select_from(rzjoin).\
|
||||
inner_q = select([recordsets_table.c.id, # 0 - RS ID
|
||||
zones_table.c.name] # 1 - ZONE NAME
|
||||
).select_from(rzjoin).\
|
||||
where(zones_table.c.deleted == '0')
|
||||
count_q = select([func.count(distinct(recordsets_table.c.id))]).\
|
||||
select_from(rzjoin).where(zones_table.c.deleted == '0')
|
||||
@ -338,17 +340,18 @@ class SQLAlchemy(object):
|
||||
# http://dev.mysql.com/doc/mysql-reslimits-excerpt/5.6/en/subquery-restrictions.html # noqa
|
||||
|
||||
inner_rproxy = self.session.execute(inner_q)
|
||||
ids = inner_rproxy.fetchall()
|
||||
if len(ids) == 0:
|
||||
rows = inner_rproxy.fetchall()
|
||||
if len(rows) == 0:
|
||||
return 0, objects.RecordSetList()
|
||||
id_zname_map = {}
|
||||
for r in rows:
|
||||
id_zname_map[r[0]] = r[1]
|
||||
formatted_ids = six.moves.map(operator.itemgetter(0), rows)
|
||||
|
||||
resultproxy = self.session.execute(count_q)
|
||||
result = resultproxy.fetchone()
|
||||
total_count = 0 if result is None else result[0]
|
||||
|
||||
# formatted_ids = [id[0] for id in ids]
|
||||
formatted_ids = six.moves.map(operator.itemgetter(0), ids)
|
||||
|
||||
# Join the 2 required tables
|
||||
rjoin = recordsets_table.outerjoin(
|
||||
records_table,
|
||||
@ -465,6 +468,9 @@ class SQLAlchemy(object):
|
||||
for key, value in rs_map.items():
|
||||
setattr(current_rrset, key, record[value])
|
||||
|
||||
current_rrset.zone_name = id_zname_map[current_rrset.id]
|
||||
current_rrset.obj_reset_changes(['zone_name'])
|
||||
|
||||
current_rrset.records = objects.RecordList()
|
||||
|
||||
if record[r_map['id']] is not None:
|
||||
|
@ -1095,14 +1095,15 @@ class CentralZoneTestCase(CentralBasic):
|
||||
name='example.org.',
|
||||
tenant_id='2',
|
||||
)
|
||||
self.service.storage.get_recordset.return_value = RoObject(
|
||||
self.service.storage.get_recordset.return_value = objects.RecordSet(
|
||||
zone_id='2',
|
||||
zone_name='example.org.',
|
||||
id='3'
|
||||
)
|
||||
self.service.get_recordset(
|
||||
self.context,
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
)
|
||||
self.assertEqual(
|
||||
'get_recordset',
|
||||
@ -1111,7 +1112,7 @@ class CentralZoneTestCase(CentralBasic):
|
||||
t, ctx, target = designate.central.service.policy.check.call_args[0]
|
||||
self.assertEqual('get_recordset', t)
|
||||
self.assertEqual({
|
||||
'zone_id': '1',
|
||||
'zone_id': '2',
|
||||
'zone_name': 'example.org.',
|
||||
'recordset_id': '3',
|
||||
'tenant_id': '2'}, target)
|
||||
@ -1342,22 +1343,26 @@ class CentralZoneTestCase(CentralBasic):
|
||||
self.service.delete_recordset(self.context, 'd', 'r')
|
||||
|
||||
def test_delete_recordset(self):
|
||||
self.service.storage.get_zone.return_value = RoObject(
|
||||
mock_zone = RoObject(
|
||||
action='foo',
|
||||
id=4,
|
||||
name='example.org.',
|
||||
tenant_id='2',
|
||||
type='foo',
|
||||
)
|
||||
self.service.storage.get_recordset.return_value = RoObject(
|
||||
mock_rs = objects.RecordSet(
|
||||
zone_id=4,
|
||||
zone_name='example.org.',
|
||||
id='i',
|
||||
managed=False,
|
||||
records=[],
|
||||
)
|
||||
|
||||
self.service.storage.get_zone.return_value = mock_zone
|
||||
self.service.storage.get_recordset.return_value = mock_rs
|
||||
self.context = Mock()
|
||||
self.context.edit_managed_records = False
|
||||
self.service._delete_recordset_in_storage = Mock(
|
||||
return_value=('', '')
|
||||
return_value=(mock_rs, mock_zone)
|
||||
)
|
||||
with fx_pool_manager:
|
||||
self.service.delete_recordset(self.context, 'd', 'r')
|
||||
|
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mock import Mock
|
||||
from oslo_log import log as logging
|
||||
import oslotest.base
|
||||
|
||||
@ -41,3 +42,15 @@ class DesignateAdapterTest(oslotest.base.BaseTestCase):
|
||||
|
||||
def test_object_render(self):
|
||||
adapters.DesignateAdapter.render('TEST_API', objects.DesignateObject())
|
||||
|
||||
|
||||
class RecordSetAPIv2AdapterTest(oslotest.base.BaseTestCase):
|
||||
def test_get_path(self):
|
||||
request = Mock()
|
||||
request.path = '/v2/recordsets'
|
||||
recordset = Mock()
|
||||
recordset.zone_id = 'a-b-c-d'
|
||||
expected_path = '/v2/zones/a-b-c-d/recordsets'
|
||||
|
||||
path = adapters.RecordSetAPIv2Adapter._get_path(request, recordset)
|
||||
self.assertEqual(expected_path, path)
|
||||
|
@ -306,8 +306,6 @@ Open up a new ssh window and log in to your server (or however you’re communic
|
||||
|
||||
$ cd openstack/designate
|
||||
|
||||
::
|
||||
|
||||
If Designate was installed into a virtualenv, make sure your virtualenv is sourced
|
||||
|
||||
::
|
||||
|
@ -99,6 +99,8 @@ The following format can be used for common record set types including A, AAAA,
|
||||
Get Record Set
|
||||
--------------
|
||||
|
||||
Two APIs can be used to retrieve a single recordset. One with zone ID in url, the other without.
|
||||
|
||||
.. http:get:: /zones/(uuid:id)/recordsets/(uuid:id)
|
||||
|
||||
Retrieves a record set with the specified record set ID.
|
||||
@ -142,18 +144,62 @@ Get Record Set
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
.. http:get:: /recordsets/(uuid:id)
|
||||
|
||||
If http client follows redirect, API returns a 200. Otherwise it returns 301 with the canonical location of the requested recordset.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"description": "This is an example recordset.",
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/f7b10e9b-0cae-4a91-b162-562bc6096648"
|
||||
},
|
||||
"updated_at": null,
|
||||
"records": [
|
||||
"10.1.0.2"
|
||||
],
|
||||
"ttl": 3600,
|
||||
"id": "f7b10e9b-0cae-4a91-b162-562bc6096648",
|
||||
"name": "example.org.",
|
||||
"zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f",
|
||||
"created_at": "2014-10-24T19:59:44.000000",
|
||||
"version": 1,
|
||||
"type": "A"
|
||||
}
|
||||
|
||||
:statuscode 301: Moved Permanently
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
List Record Sets
|
||||
----------------
|
||||
|
||||
.. http:get:: /zones/(uuid:id)/recordsets
|
||||
**Lists all record sets for a given zone**
|
||||
|
||||
Lists all record sets for a given zone id.
|
||||
.. http:get:: /zones/(uuid:id)/recordsets
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets HTTP/1.1
|
||||
GET /v2/zones/c991f02b-ae05-4570-bf75-73def68fe700/recordsets HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@ -237,6 +283,192 @@ List Record Sets
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
**Lists record sets across all zones**
|
||||
|
||||
.. http:get:: /recordsets
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/recordsets HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
**Example Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"recordsets": [
|
||||
{
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/65ee6b49-bb4c-4e52-9799-31330c94161f"
|
||||
},
|
||||
"updated_at": null,
|
||||
"records": [
|
||||
"ns1.devstack.org."
|
||||
],
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"id": "65ee6b49-bb4c-4e52-9799-31330c94161f",
|
||||
"name": "example.org.",
|
||||
"zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f",
|
||||
"zone_name": "example.org.",
|
||||
"created_at": "2014-10-24T19:59:11.000000",
|
||||
"version": 1,
|
||||
"type": "NS"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/2150b1bf-dee2-4221-9d85-11f7886fb15f/recordsets/14500cf9-bdff-48f6-b06b-5fc7491ffd9e"
|
||||
},
|
||||
"updated_at": "2014-10-24T19:59:46.000000",
|
||||
"records": [
|
||||
"ns1.devstack.org. jli.ex.com. 1458666091 3502 600 86400 3600"
|
||||
],
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"id": "14500cf9-bdff-48f6-b06b-5fc7491ffd9e",
|
||||
"name": "example.org.",
|
||||
"zone_id": "2150b1bf-dee2-4221-9d85-11f7886fb15f",
|
||||
"zone_name": "example.org.",
|
||||
"created_at": "2014-10-24T19:59:12.000000",
|
||||
"version": 1,
|
||||
"type": "SOA"
|
||||
},
|
||||
{
|
||||
"name": "example.com.",
|
||||
"id": "12caacfd-f0fc-4bcb-aa24-c42769897822",
|
||||
"type": "SOA",
|
||||
"zone_name": "example.com.",
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/b8d7eaf1-e5c7-4b15-be6e-4b2809f47ec3/recordsets/12caacfd-f0fc-4bcb-aa24-c42769897822"
|
||||
},
|
||||
"created_at": "2016-03-22T16:12:35.000000",
|
||||
"updated_at": "2016-03-22T17:01:31.000000",
|
||||
"records": [
|
||||
"ns1.devstack.org. jli.ex.com. 1458666091 3502 600 86400 3600"
|
||||
],
|
||||
"zone_id": "b8d7eaf1-e5c7-4b15-be6e-4b2809f47ec3",
|
||||
"version": 2
|
||||
},
|
||||
{
|
||||
"name": "example.com.",
|
||||
"id": "f39c51d1-ec2c-48a8-b9f7-877d56b7b82a",
|
||||
"type": "NS",
|
||||
"zone_name": "example.com.",
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/b8d7eaf1-e5c7-4b15-be6e-4b2809f47ec3/recordsets/f39c51d1-ec2c-48a8-b9f7-877d56b7b82a"
|
||||
},
|
||||
"created_at": "2016-03-22T16:12:35.000000",
|
||||
"updated_at": null,
|
||||
"records": [
|
||||
"ns1.devstack.org."
|
||||
],
|
||||
"zone_id": "b8d7eaf1-e5c7-4b15-be6e-4b2809f47ec3",
|
||||
"version": 1
|
||||
},
|
||||
],
|
||||
"metadata": {
|
||||
"total_count": 4
|
||||
},
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/recordsets"
|
||||
}
|
||||
}
|
||||
|
||||
**Filtering record sets**
|
||||
|
||||
.. http:get:: /recordsets?KEY=VALUE
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/recordsets?data=192.168* HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
**Example Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"metadata": {
|
||||
"total_count": 2
|
||||
},
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/recordsets?data=192.168%2A"
|
||||
},
|
||||
"recordsets": [
|
||||
{
|
||||
"name": "mail.example.net.",
|
||||
"id": "a48588c5-5093-4585-b0fc-3e399d169c01",
|
||||
"type": "A",
|
||||
"zone_name": "example.net.",
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/601a25f0-5c4d-4058-8d9c-e6a78f5ffbb8/recordsets/a48588c5-5093-4585-b0fc-3e399d169c01"
|
||||
},
|
||||
"created_at": "2016-04-04T20:11:08.000000",
|
||||
"updated_at": null,
|
||||
"records": [
|
||||
"192.168.0.1"
|
||||
],
|
||||
"zone_id": "601a25f0-5c4d-4058-8d9c-e6a78f5ffbb8",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "www.example.net.",
|
||||
"id": "f2c7a0f6-8ec7-4d14-b8ec-2a55a8129160",
|
||||
"type": "A",
|
||||
"zone_name": "example.net.",
|
||||
"action": "NONE",
|
||||
"ttl": null,
|
||||
"status": "ACTIVE",
|
||||
"description": null,
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/601a25f0-5c4d-4058-8d9c-e6a78f5ffbb8/recordsets/f2c7a0f6-8ec7-4d14-b8ec-2a55a8129160"
|
||||
},
|
||||
"created_at": "2016-04-04T22:21:03.000000",
|
||||
"updated_at": null,
|
||||
"records": [
|
||||
"192.168.6.6"
|
||||
],
|
||||
"zone_id": "601a25f0-5c4d-4058-8d9c-e6a78f5ffbb8",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Update Record Set
|
||||
-----------------
|
||||
|
||||
|
@ -24,20 +24,27 @@ from functionaltests.common import utils
|
||||
|
||||
class RecordsetClient(ClientMixin):
|
||||
|
||||
def recordsets_uri(self, zone_id, filters=None):
|
||||
return self.create_uri("/zones/{0}/recordsets".format(zone_id),
|
||||
filters=filters)
|
||||
def recordsets_uri(self, zone_id, cross_zone=False, filters=None):
|
||||
if cross_zone:
|
||||
uri = self.create_uri("/recordsets", filters=filters)
|
||||
else:
|
||||
uri = self.create_uri("/zones/{0}/recordsets".format(zone_id),
|
||||
filters=filters)
|
||||
return uri
|
||||
|
||||
def recordset_uri(self, zone_id, recordset_id):
|
||||
return "{0}/{1}".format(self.recordsets_uri(zone_id), recordset_id)
|
||||
def recordset_uri(self, zone_id, recordset_id, cross_zone=False):
|
||||
return "{0}/{1}".format(self.recordsets_uri(zone_id, cross_zone),
|
||||
recordset_id)
|
||||
|
||||
def list_recordsets(self, zone_id, filters=None, **kwargs):
|
||||
def list_recordsets(self, zone_id, cross_zone=False, filters=None,
|
||||
**kwargs):
|
||||
resp, body = self.client.get(
|
||||
self.recordsets_uri(zone_id, filters), **kwargs)
|
||||
self.recordsets_uri(zone_id, cross_zone, filters), **kwargs)
|
||||
return self.deserialize(resp, body, RecordsetListModel)
|
||||
|
||||
def get_recordset(self, zone_id, recordset_id, **kwargs):
|
||||
resp, body = self.client.get(self.recordset_uri(zone_id, recordset_id),
|
||||
def get_recordset(self, zone_id, recordset_id, cross_zone=False, **kwargs):
|
||||
resp, body = self.client.get(self.recordset_uri(zone_id, recordset_id,
|
||||
cross_zone),
|
||||
**kwargs)
|
||||
return self.deserialize(resp, body, RecordsetModel)
|
||||
|
||||
|
@ -222,3 +222,65 @@ class RecordsetOwnershipTest(DesignateV2Test):
|
||||
self.assertRaises(exceptions.RestClientException,
|
||||
lambda: RecordsetClient.as_user('alt')
|
||||
.post_recordset(alt_zone.id, recordset))
|
||||
|
||||
|
||||
@utils.parameterized_class
|
||||
class RecordsetCrossZoneTest(DesignateV2Test):
|
||||
|
||||
def setUp(self):
|
||||
super(RecordsetCrossZoneTest, self).setUp()
|
||||
self.increase_quotas(user='default')
|
||||
self.ensure_tld_exists('com')
|
||||
self.zone = self.useFixture(ZoneFixture()).created_zone
|
||||
self.alt_zone = self.useFixture(ZoneFixture()).created_zone
|
||||
|
||||
def test_get_single_recordset(self):
|
||||
post_model = datagen.random_a_recordset(self.zone.name)
|
||||
_, resp_model = RecordsetClient.as_user('default').post_recordset(
|
||||
self.zone.id, post_model)
|
||||
rrset_id = resp_model.id
|
||||
|
||||
resp, model = RecordsetClient.as_user('default').get_recordset(
|
||||
self.zone.id, rrset_id, cross_zone=True)
|
||||
self.assertEqual(200, resp.status)
|
||||
|
||||
# clean up
|
||||
RecordsetClient.as_user('default').delete_recordset(self.zone.id,
|
||||
rrset_id)
|
||||
|
||||
def test_list_recordsets(self):
|
||||
post_model = datagen.random_a_recordset(self.zone.name)
|
||||
self.useFixture(RecordsetFixture(self.zone.id, post_model))
|
||||
post_model = datagen.random_a_recordset(self.alt_zone.name)
|
||||
self.useFixture(RecordsetFixture(self.alt_zone.id, post_model))
|
||||
|
||||
resp, model = RecordsetClient.as_user('default').list_recordsets(
|
||||
'zone_id', cross_zone=True)
|
||||
self.assertEqual(200, resp.status)
|
||||
zone_names = set()
|
||||
for r in model.recordsets:
|
||||
zone_names.add(r.zone_name)
|
||||
self.assertGreaterEqual(len(zone_names), 2)
|
||||
|
||||
def test_filter_recordsets(self):
|
||||
# create one A recordset in 'zone'
|
||||
post_model = datagen.random_a_recordset(self.zone.name,
|
||||
ip='123.201.99.1')
|
||||
self.useFixture(RecordsetFixture(self.zone.id, post_model))
|
||||
|
||||
# create two A recordsets in 'alt_zone'
|
||||
post_model = datagen.random_a_recordset(self.alt_zone.name,
|
||||
ip='10.0.1.1')
|
||||
self.useFixture(RecordsetFixture(self.alt_zone.id, post_model))
|
||||
post_model = datagen.random_a_recordset(self.alt_zone.name,
|
||||
ip='123.201.99.2')
|
||||
self.useFixture(RecordsetFixture(self.alt_zone.id, post_model))
|
||||
|
||||
# Add limit in filter to make response paginated
|
||||
filters = {"data": "123.201.99.*", "limit": 2}
|
||||
resp, model = RecordsetClient.as_user('default') \
|
||||
.list_recordsets('zone_id', cross_zone=True, filters=filters)
|
||||
self.assertEqual(200, resp.status)
|
||||
self.assertEqual(2, model.metadata.total_count)
|
||||
self.assertEqual(len(model.recordsets), 2)
|
||||
self.assertIsNotNone(model.links.next)
|
||||
|
5
releasenotes/notes/recordset-api-2c82abf569f7623e.yaml
Normal file
5
releasenotes/notes/recordset-api-2c82abf569f7623e.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- A new recordset api ``/v2/recordsets`` is exposed with GET method
|
||||
allowed only. The api can be used for retrieving recordsets across all the
|
||||
zones under a tenant. Filtering on certain fields is supported as well.
|
Loading…
Reference in New Issue
Block a user