Added functionality to allow for zone ownership transfers

This includes 3 new API endpoints in the v2 API

/v2/zones/<id>/tasks/transfer_requests
/v2/zones/tasks/transfer_requests
/v2/zones/tasks/transfer_accepts

Change-Id: Ic5fd5a3b0a309346fbb72407771f88f8dbcfe17c
implements: zone-migration-between-tenants
This commit is contained in:
Graham Hayes 2014-06-23 13:51:04 +01:00
parent eb04d93ca3
commit be27456560
32 changed files with 2230 additions and 19 deletions

View File

@ -93,6 +93,14 @@ class RestController(pecan.rest.RestController):
return marker, limit, sort_key, sort_dir
def _apply_filter_params(self, params, accepted_filters, criterion):
for k in accepted_filters:
if k in params:
criterion[k] = params[k]
return criterion
def _handle_post(self, method, remainder):
'''
Routes ``POST`` actions to the appropriate controller.

View File

@ -21,10 +21,10 @@ from designate.api.v2.controllers import limits
from designate.api.v2.controllers import reverse
from designate.api.v2.controllers import schemas
from designate.api.v2.controllers import tlds
from designate.api.v2.controllers import zones
from designate.api.v2.controllers import blacklists
from designate.api.v2.controllers import errors
from designate.api.v2.controllers import pools
from designate.api.v2.controllers import zones
LOG = logging.getLogger(__name__)

View File

@ -24,6 +24,7 @@ from designate import dnsutils
from designate.api.v2.controllers import rest
from designate.api.v2.controllers import nameservers
from designate.api.v2.controllers import recordsets
from designate.api.v2.controllers.zones import tasks
from designate.api.v2.views import zones as zones_view
from designate.objects import Domain
@ -37,6 +38,7 @@ class ZonesController(rest.RestController):
nameservers = nameservers.NameServersController()
recordsets = recordsets.RecordSetsController()
tasks = tasks.TasksController()
@pecan.expose(template='json:', content_type='application/json')
@pecan.expose(template=None, content_type='text/dns')

View File

@ -0,0 +1,29 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.controllers import rest
from designate.api.v2.controllers.zones.tasks.transfer_requests \
import TransferRequestsController as TRC
from designate.api.v2.controllers.zones.tasks.transfer_accepts \
import TransferAcceptsController as TRA
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class TasksController(rest.RestController):
transfer_accepts = TRA()
transfer_requests = TRC()

View File

@ -0,0 +1,72 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.openstack.common import log as logging
from designate import schema
from designate import utils
from designate.api.v2.controllers import rest
from designate.api.v2.views.zones.tasks import transfer_accepts as \
zone_transfer_accepts_view
from designate.objects import ZoneTransferAccept
LOG = logging.getLogger(__name__)
class TransferAcceptsController(rest.RestController):
_view = zone_transfer_accepts_view.ZoneTransferAcceptsView()
_resource_schema = schema.Schema('v2', 'transfer_accept')
_collection_schema = schema.Schema('v2', 'transfer_accepts')
SORT_KEYS = ['created_at', 'id', 'updated_at']
@pecan.expose(template='json:', content_type='application/json')
@utils.validate_uuid('transfer_accept_id')
def get_one(self, transfer_accept_id):
"""Get transfer_accepts"""
request = pecan.request
context = request.environ['context']
transfer_accepts = \
self.central_api.get_zone_transfer_accept(
context, transfer_accept_id)
return self._view.show(context, request, transfer_accepts)
@pecan.expose(template='json:', content_type='application/json')
def post_all(self):
"""Create ZoneTransferAccept"""
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_transfer_request
zone_transfer_accept = self.central_api.create_zone_transfer_accept(
context, ZoneTransferAccept(**values))
response.status_int = 201
response.headers['Location'] = self._view._get_resource_href(
request,
zone_transfer_accept)
# Prepare and return the response body
return self._view.show(context, request, zone_transfer_accept)

View File

@ -0,0 +1,144 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.openstack.common import log as logging
from designate import schema
from designate import utils
from designate.api.v2.controllers import rest
from designate.api.v2.views.zones.tasks import transfer_requests as \
zone_transfer_requests_view
from designate.objects import ZoneTransferRequest
LOG = logging.getLogger(__name__)
class TransferRequestsController(rest.RestController):
_view = zone_transfer_requests_view.ZoneTransferRequestsView()
_resource_schema = schema.Schema('v2', 'transfer_request')
_collection_schema = schema.Schema('v2', 'transfer_requests')
SORT_KEYS = ['created_at', 'id', 'updated_at']
@pecan.expose(template='json:', content_type='application/json')
@utils.validate_uuid('transfer_request_id')
def get_one(self, transfer_request_id):
"""Get transfer_request"""
request = pecan.request
context = request.environ['context']
transfer_request = \
self.central_api.get_zone_transfer_request(
context, transfer_request_id)
return self._view.show(context, request, transfer_request)
@pecan.expose(template='json:', content_type='application/json')
def get_all(self, **params):
"""List ZoneTransferRequests"""
request = pecan.request
context = request.environ['context']
# Extract the pagination params
marker, limit, sort_key, sort_dir = self._get_paging_params(params)
# Extract any filter params.
criterion = self._apply_filter_params(params, ('status',), {})
zone_transfer_requests = self.central_api.find_zone_transfer_requests(
context, criterion, marker, limit, sort_key, sort_dir)
return self._view.list(context, request, zone_transfer_requests)
@pecan.expose(template='json:', content_type='application/json')
@utils.validate_uuid('zone_id')
def post_all(self, zone_id):
"""Create ZoneTransferRequest"""
request = pecan.request
response = pecan.response
context = request.environ['context']
body = request.body_dict
if body['transfer_request'] is not None:
body['transfer_request']['zone_id'] = zone_id
# 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_transfer_request
zone_transfer_request = self.central_api.create_zone_transfer_request(
context, ZoneTransferRequest(**values))
response.status_int = 201
response.headers['Location'] = self._view._get_resource_href(
request,
zone_transfer_request)
# Prepare and return the response body
return self._view.show(context, request, zone_transfer_request)
@pecan.expose(template='json:', content_type='application/json')
@pecan.expose(template='json:', content_type='application/json-patch+json')
@utils.validate_uuid('zone_transfer_request_id')
def patch_one(self, zone_transfer_request_id):
"""Update ZoneTransferRequest"""
request = pecan.request
context = request.environ['context']
body = request.body_dict
response = pecan.response
# Fetch the existing zone_transfer_request
zt_request = self.central_api.get_zone_transfer_request(
context, zone_transfer_request_id)
# Convert to APIv2 Format
zt_request_data = self._view.show(context,
request, zt_request)
if request.content_type == 'application/json-patch+json':
raise NotImplemented('json-patch not implemented')
else:
zt_request_data = utils.deep_dict_merge(
zt_request_data, body)
# Validate the request conforms to the schema
self._resource_schema.validate(zt_request_data)
zt_request.update(self._view.load(context, request, body))
zt_request = self.central_api.update_zone_transfer_request(
context, zt_request)
response.status_int = 200
return self._view.show(context, request, zt_request)
@pecan.expose(template=None, content_type='application/json')
@utils.validate_uuid('zone_transfer_request_id')
def delete_one(self, zone_transfer_request_id):
"""Delete ZoneTransferRequest"""
request = pecan.request
response = pecan.response
context = request.environ['context']
self.central_api.delete_zone_transfer_request(
context, zone_transfer_request_id)
response.status_int = 204
# NOTE: This is a hack and a half.. But Pecan needs it.
return ''

View File

@ -0,0 +1,52 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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 ZoneTransferAcceptsView(base_view.BaseView):
"""Model a ZoneTransferRequest API response as a python dictionary"""
_resource_name = 'transfer_accept'
_collection_name = 'transfer_accepts'
def _get_base_href(self, parents=None):
href = "%s/v2/zones/tasks/%s" % (self.base_uri, self._collection_name)
return href.rstrip('?')
def _get_resource_links(self, request, item):
return {
"self": self._get_resource_href(request, item),
"zone": "%s/v2/zones/%s" % (self.base_uri, item.domain_id)
}
def show_basic(self, context, request, zt_accept):
"""Basic view of a ZoneTransferRequest"""
return {
"id": zt_accept.id,
"status": zt_accept.status,
"links": self._get_resource_links(request, zt_accept)
}
def load(self, context, request, body):
"""Extract a "central" compatible dict from an API call"""
valid_keys = ('zone_transfer_request_id', 'key')
return self._load(context, request, body, valid_keys)

View File

@ -0,0 +1,84 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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
from designate import exceptions
from designate import policy
LOG = logging.getLogger(__name__)
class ZoneTransferRequestsView(base_view.BaseView):
"""Model a ZoneTransferRequest API response as a python dictionary"""
_resource_name = 'transfer_request'
_collection_name = 'transfer_requests'
def _get_base_href(self, parents=None):
href = "%s/v2/zones/tasks/%s" % (self.base_uri, self._collection_name)
return href.rstrip('?')
def show_basic(self, context, request, zt_request):
"""Basic view of a ZoneTransferRequest"""
try:
target = {
'tenant_id': zt_request.tenant_id,
}
policy.check('get_zone_transfer_request_detailed', context, target)
except exceptions.Forbidden:
return {
"id": zt_request.id,
"description": zt_request.description,
"zone_id": zt_request.domain_id,
"zone_name": zt_request.domain_name,
"status": zt_request.status,
"links": self._get_resource_links(request, zt_request)
}
else:
return {
"id": zt_request.id,
"description": zt_request.description,
"zone_id": zt_request.domain_id,
"zone_name": zt_request.domain_name,
"target_project_id": zt_request.target_tenant_id,
"project_id": zt_request.tenant_id,
"created_at": zt_request.created_at,
"updated_at": zt_request.updated_at,
"status": zt_request.status,
"key": zt_request.key,
"links": self._get_resource_links(request, zt_request)
}
def load(self, context, request, body):
"""Extract a "central" compatible dict from an API call"""
valid_keys = ('description', 'domain_id', 'target_tenant_id')
zt_request = body["transfer_request"]
old_keys = {
'zone_id': 'domain_id',
'project_id': 'tenant_id',
'target_project_id': 'target_tenant_id',
}
for key in zt_request:
if key in old_keys:
zt_request[old_keys[key]] = ''
zt_request[old_keys[key]] = zt_request.pop(key)
return self._load(context, request, body, valid_keys)

View File

@ -43,14 +43,16 @@ class CentralAPI(object):
4.0 - Create methods now accept designate objects
4.1 - Add methods for server pools
4.2 - Add methods for pool manager integration
4.3 - Added Zone Transfer Methods
"""
RPC_API_VERSION = '4.2'
RPC_API_VERSION = '4.3'
def __init__(self, topic=None):
topic = topic if topic else cfg.CONF.central_topic
target = messaging.Target(topic=topic, version=self.RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='4.2')
self.client = rpc.get_client(target, version_cap='4.3')
# Misc Methods
def get_absolute_limits(self, context):
@ -413,3 +415,108 @@ class CentralAPI(object):
cctxt = self.client.prepare(version='4.2')
return cctxt.call(context, 'update_status', domain_id=domain_id,
status=status, serial=serial)
# Zone Ownership Transfers
def create_zone_transfer_request(self, context, zone_transfer_request):
LOG.info(_LI("create_zone_transfer_request: \
Calling central's create_zone_transfer_request."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'create_zone_transfer_request',
zone_transfer_request=zone_transfer_request)
def get_zone_transfer_request(self, context, zone_transfer_request_id):
LOG.info(_LI("get_zone_transfer_request: \
Calling central's get_zone_transfer_request."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context,
'get_zone_transfer_request',
zone_transfer_request_id=zone_transfer_request_id)
def find_zone_transfer_requests(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
LOG.info(_LI("find_zone_transfer_requests: \
Calling central's find_zone_transfer_requests."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'find_zone_transfer_requests', criterion=criterion,
marker=marker, limit=limit, sort_key=sort_key, sort_dir=sort_dir)
def find_zone_transfer_request(self, context, zone_transfer_request):
LOG.info(_LI("find_zone_transfer_request: \
Calling central's find_zone_transfer_request."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'find_zone_transfer_request',
zone_transfer_request=zone_transfer_request)
def update_zone_transfer_request(self, context, zone_transfer_request):
LOG.info(_LI("update_zone_transfer_request: \
Calling central's update_zone_transfer_request."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'update_zone_transfer_request',
zone_transfer_request=zone_transfer_request)
def delete_zone_transfer_request(self, context, zone_transfer_request_id):
LOG.info(_LI("delete_zone_transfer_request: \
Calling central's delete_zone_transfer_request."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context,
'delete_zone_transfer_request',
zone_transfer_request_id=zone_transfer_request_id)
def create_zone_transfer_accept(self, context, zone_transfer_accept):
LOG.info(_LI("create_zone_transfer_accept: \
Calling central's create_zone_transfer_accept."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'create_zone_transfer_accept',
zone_transfer_accept=zone_transfer_accept)
def get_zone_transfer_accept(self, context, zone_transfer_accept_id):
LOG.info(_LI("get_zone_transfer_accept: \
Calling central's get_zone_transfer_accept."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context,
'get_zone_transfer_accept',
zone_transfer_accept_id=zone_transfer_accept_id)
def find_zone_transfer_accepts(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
LOG.info(_LI("find_zone_transfer_accepts: \
Calling central's find_zone_transfer_accepts."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'find_zone_transfer_accepts', criterion=criterion,
marker=marker, limit=limit, sort_key=sort_key, sort_dir=sort_dir)
def find_zone_transfer_accept(self, context, zone_transfer_accept):
LOG.info(_LI("find_zone_transfer_accept: \
Calling central's find_zone_transfer_accept."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'find_zone_transfer_accept',
zone_transfer_accept=zone_transfer_accept)
def update_zone_transfer_accept(self, context, zone_transfer_accept):
LOG.info(_LI("update_zone_transfer_accept: \
Calling central's update_zone_transfer_accept."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context, 'update_zone_transfer_accept',
zone_transfer_accept=zone_transfer_accept)
def delete_zone_transfer_accept(self, context, zone_transfer_accept_id):
LOG.info(_LI("delete_zone_transfer_accept: \
Calling central's delete_zone_transfer_accept."))
cctxt = self.client.prepare(version='4.3')
return cctxt.call(
context,
'delete_zone_transfer_accept',
zone_transfer_accept_id=zone_transfer_accept_id)

View File

@ -1,5 +1,5 @@
# Copyright 2012 Managed I.T.
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# Copyright 2013 - 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
@ -20,6 +20,8 @@ import collections
import functools
import threading
import itertools
import string
import random
from oslo.config import cfg
from oslo import messaging
@ -113,7 +115,9 @@ def synchronized_domain(domain_arg=1, new_domain=False):
break
elif (isinstance(arg, objects.RecordSet) or
isinstance(arg, objects.Record)):
isinstance(arg, objects.Record) or
isinstance(arg, objects.ZoneTransferRequest) or
isinstance(arg, objects.ZoneTransferAccept)):
domain_id = arg.domain_id
if domain_id is not None:
@ -196,7 +200,7 @@ def notification(notification_type):
class Service(service.RPCService):
RPC_API_VERSION = '4.2'
RPC_API_VERSION = '4.3'
target = messaging.Target(version=RPC_API_VERSION)
@ -1716,10 +1720,11 @@ class Service(service.RPCService):
zone = self.storage.find_domain(
elevated_context, {'name': zone_name})
except exceptions.DomainNotFound:
msg = _LI('Creating zone for %(fip_id)s:%(region)s - '
'%(fip_addr)s zone %(zonename)s') % \
{'fip_id': floatingip_id, 'region': region,
'fip_addr': fip['address'], 'zonename': zone_name}
msg = _LI(
'Creating zone for %(fip_id)s:%(region)s - '
'%(fip_addr)s zone %(zonename)s') % \
{'fip_id': floatingip_id, 'region': region,
'fip_addr': fip['address'], 'zonename': zone_name}
LOG.info(msg)
email = cfg.CONF['service:central'].managed_resource_email
@ -1737,7 +1742,7 @@ class Service(service.RPCService):
record_name = self.network_api.address_name(fip['address'])
try:
# NOTE: Delete the current recormdset if any (also purges records)
# NOTE: Delete the current recordset if any (also purges records)
LOG.debug("Removing old RRset / Record")
rset = self.find_recordset(
elevated_context, {'name': record_name, 'type': 'PTR'})
@ -1987,3 +1992,207 @@ class Service(service.RPCService):
self.storage.update_record(context, record)
self.storage.update_domain(context, domain)
# Zone Transfers
def _transfer_key_generator(self, size=8):
chars = string.ascii_uppercase + string.digits
return ''.join(random.choice(chars) for _ in range(size))
@notification('dns.zone_transfer_request.create')
@transaction
def create_zone_transfer_request(self, context, zone_transfer_request):
elevated_context = context.elevated()
elevated_context.all_tenants = True
# get zone
zone = self.get_domain(context, zone_transfer_request.domain_id)
target = {
'tenant_id': zone.tenant_id,
}
policy.check('create_zone_transfer_request', context, target)
zone_transfer_request.key = self._transfer_key_generator()
if zone_transfer_request.tenant_id is None:
zone_transfer_request.tenant_id = context.tenant
created_zone_transfer_request = \
self.storage.create_zone_transfer_request(
context, zone_transfer_request)
return created_zone_transfer_request
def get_zone_transfer_request(self, context, zone_transfer_request_id):
elevated_context = context.elevated()
elevated_context.all_tenants = True
# Get zone transfer request
zone_transfer_request = self.storage.get_zone_transfer_request(
elevated_context, zone_transfer_request_id)
LOG.info(_LI('Target Tenant ID found - using scoped policy'))
target = {
'target_tenant_id': zone_transfer_request.target_tenant_id,
'tenant_id': zone_transfer_request.tenant_id,
}
policy.check('get_zone_transfer_request', context, target)
return zone_transfer_request
def find_zone_transfer_requests(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
policy.check('find_zone_transfer_requests', context)
requests = self.storage.find_zone_transfer_requests(
context, criterion,
marker, limit,
sort_key, sort_dir)
return requests
def find_zone_transfer_request(self, context, criterion):
target = {
'tenant_id': context.tenant,
}
policy.check('find_zone_transfer_request', context, target)
return self.storage.find_zone_transfer_requests(context, criterion)
@notification('dns.zone_transfer_request.update')
@transaction
def update_zone_transfer_request(self, context, zone_transfer_request):
if 'domain_id' in zone_transfer_request.obj_what_changed():
raise exceptions.InvalidOperation('Domain cannot be changed')
target = {
'tenant_id': zone_transfer_request.tenant_id,
}
policy.check('update_zone_transfer_request', context, target)
request = self.storage.update_zone_transfer_request(
context, zone_transfer_request)
return request
@notification('dns.zone_transfer_request.delete')
@transaction
def delete_zone_transfer_request(self, context, zone_transfer_request_id):
# Get zone transfer request
zone_transfer_request = self.storage.get_zone_transfer_request(
context, zone_transfer_request_id)
target = {
'tenant_id': zone_transfer_request.tenant_id,
}
policy.check('delete_zone_transfer_request', context, target)
return self.storage.delete_zone_transfer_request(
context,
zone_transfer_request_id)
@notification('dns.zone_transfer_accept.create')
@transaction
def create_zone_transfer_accept(self, context, zone_transfer_accept):
elevated_context = context.elevated()
elevated_context.all_tenants = True
zone_transfer_request = self.get_zone_transfer_request(
context, zone_transfer_accept.zone_transfer_request_id)
zone_transfer_accept.domain_id = zone_transfer_request.domain_id
if zone_transfer_request.status != 'ACTIVE':
if zone_transfer_request.status == 'COMPLETE':
raise exceptions.InvaildZoneTransfer(
'Zone Transfer Request has been used')
raise exceptions.InvaildZoneTransfer(
'Zone Transfer Request Invalid')
if zone_transfer_request.key != zone_transfer_accept.key:
raise exceptions.IncorrectZoneTransferKey(
'Key does not match stored key for request')
target = {
'target_tenant_id': zone_transfer_request.target_tenant_id
}
policy.check('create_zone_transfer_accept', context, target)
if zone_transfer_accept.tenant_id is None:
zone_transfer_accept.tenant_id = context.tenant
created_zone_transfer_accept = \
self.storage.create_zone_transfer_accept(
context, zone_transfer_accept)
try:
domain = self.storage.get_domain(
elevated_context,
zone_transfer_request.domain_id)
domain.tenant_id = zone_transfer_accept.tenant_id
self.storage.update_domain(elevated_context, domain)
except Exception:
created_zone_transfer_accept.status = 'ERROR'
self.storage.update_zone_transfer_accept(
context, created_zone_transfer_accept)
raise
else:
created_zone_transfer_accept.status = 'COMPLETE'
zone_transfer_request.status = 'COMPLETE'
self.storage.update_zone_transfer_accept(
context, created_zone_transfer_accept)
self.storage.update_zone_transfer_request(
elevated_context, zone_transfer_request)
return created_zone_transfer_accept
def get_zone_transfer_accept(self, context, zone_transfer_accept_id):
# Get zone transfer accept
zone_transfer_accept = self.storage.get_zone_transfer_accept(
context, zone_transfer_accept_id)
target = {
'tenant_id': zone_transfer_accept.tenant_id
}
policy.check('get_zone_transfer_accept', context, target)
return zone_transfer_accept
def find_zone_transfer_accepts(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
policy.check('find_zone_transfer_accepts', context)
return self.storage.find_zone_transfer_accepts(context, criterion,
marker, limit,
sort_key, sort_dir)
def find_zone_transfer_accept(self, context, criterion):
policy.check('find_zone_transfer_accept', context)
return self.storage.find_zone_transfer_accept(context, criterion)
@notification('dns.zone_transfer_accept.update')
@transaction
def update_zone_transfer_accept(self, context, zone_transfer_accept):
target = {
'tenant_id': zone_transfer_accept.tenant_id
}
policy.check('update_zone_transfer_accept', context, target)
accept = self.storage.update_zone_transfer_accept(
context, zone_transfer_accept)
return accept
@notification('dns.zone_transfer_accept.delete')
@transaction
def delete_zone_transfer_accept(self, context, zone_transfer_accept_id):
# Get zone transfer accept
zt_accept = self.storage.get_zone_transfer_accept(
context, zone_transfer_accept_id)
target = {
'tenant_id': zt_accept.tenant_id
}
policy.check('delete_zone_transfer_accept', context, target)
return self.storage.delete_zone_transfer_accept(
context,
zone_transfer_accept_id)

View File

@ -159,6 +159,11 @@ class InvalidRecordSetLocation(Base):
error_type = 'invalid_recordset_location'
class InvaildZoneTransfer(Base):
error_code = 400
error_type = 'invalid_zone_transfer_request'
class InvalidTTL(Base):
error_code = 400
error_type = 'invalid_ttl'
@ -175,6 +180,10 @@ class Forbidden(Base):
expected = True
class IncorrectZoneTransferKey(Forbidden):
error_type = 'invalid_key'
class Duplicate(Base):
expected = True
error_code = 409
@ -231,6 +240,14 @@ class MethodNotAllowed(Base):
error_type = 'method_not_allowed'
class DuplicateZoneTransferRequest(Duplicate):
error_type = 'duplicate_zone_transfer_request'
class DuplicateZoneTransferAccept(Duplicate):
error_type = 'duplicate_zone_transfer_accept'
class NotFound(Base):
expected = True
error_code = 404
@ -285,6 +302,14 @@ class PoolAttributeNotFound(NotFound):
error_type = 'pool_attribute_not_found'
class ZoneTransferRequestNotFound(NotFound):
error_type = 'zone_transfer_request_not_found'
class ZoneTransferAcceptNotFound(NotFound):
error_type = 'zone_transfer_accept_not_found'
class LastServerDeleteNotAllowed(BadRequest):
error_type = 'last_server_delete_not_allowed'

View File

@ -46,3 +46,5 @@ from designate.objects.tld import Tld, TldList # noqa
from designate.objects.tsigkey import TsigKey, TsigKeyList # noqa
from designate.objects.validation_error import ValidationError # noqa
from designate.objects.validation_error import ValidationErrorList # noqa
from designate.objects.zone_transfer_request import ZoneTransferRequest, ZoneTransferRequestList # noqa
from designate.objects.zone_transfer_accept import ZoneTransferAccept, ZoneTransferAcceptList # noqa

View File

@ -0,0 +1,31 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.objects import base
class ZoneTransferAccept(base.DictObjectMixin, base.PersistentObjectMixin,
base.DesignateObject):
FIELDS = {
'zone_transfer_request_id': {},
'tenant_id': {},
'status': {},
'key': {},
'domain_id': {},
}
class ZoneTransferAcceptList(base.ListObjectMixin, base.DesignateObject):
LIST_ITEM_TYPE = ZoneTransferAccept

View File

@ -0,0 +1,37 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.objects import base
class ZoneTransferRequest(base.DictObjectMixin, base.DesignateObject,
base.PersistentObjectMixin):
FIELDS = {
'domain_id': {},
'key': {},
'description': {},
'tenant_id': {},
'target_tenant_id': {},
'status': {},
'id': {},
'created_at': {},
'domain_name': {},
'updated_at': {},
'version': {},
}
class ZoneTransferRequestList(base.ListObjectMixin, base.DesignateObject):
LIST_ITEM_TYPE = ZoneTransferRequest

View File

@ -81,7 +81,6 @@ def init(default_rule=None):
def check(rule, ctxt, target=None, do_raise=True, exc=exceptions.Forbidden):
creds = ctxt.to_dict()
target = target or {}
try:
result = _ENFORCER.enforce(rule, target, creds, do_raise, exc)
except Exception:

View File

@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zone_transfer_accept",
"title": "zone_transfer_accept",
"description": "Zone Transfer Accept",
"additionalProperties": false,
"required": ["transfer_accept"],
"properties": {
"transfer_accept": {
"type": "object",
"additionalProperties": false,
"required": ["zone_transfer_request_id", "key"],
"properties": {
"id": {
"type": "string",
"description": "Zone Transfer Request identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"zone_transfer_request_id": {
"type": "string",
"description": "Request identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$"
},
"key": {
"type": "string",
"description": "Password used to complete the transfer",
"maxLength": 160
},
"status": {
"type": "string",
"description": "Zone Status",
"enum": ["ACTIVE", "PENDING", "DELETED", "ERROR", "COMPLETE"],
"readOnly": true
},
"created_at": {
"type": "string",
"description": "Date and time of Request creation",
"format": "date-time",
"readOnly": true
},
"updated_at": {
"type": ["string", "null"],
"description": "Date and time of last Request modification",
"format": "date-time",
"readOnly": true
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
}
}
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zone_transfer_accept",
"title": "zone_transfer_accept",
"description": "Zone Transfer Accept",
"additionalProperties": false,
"required": ["transfer_accepts"],
"properties": {
"transfer_accepts": {
"type": "array",
"description": "Zone Transfer Requests",
"items": {"$ref": "transfer_accept#/properties/transfer_accept"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": ["string", "null"],
"format": "url"
},
"previous": {
"type": ["string", "null"],
"format": "url"
}
}
}
}
}

View File

@ -0,0 +1,90 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zone_transfer_request",
"title": "zone_transfer_request",
"description": "Zone Transfer Request",
"additionalProperties": false,
"required": ["transfer_request"],
"properties": {
"transfer_request": {
"type": "object",
"additionalProperties": false,
"required": ["zone_id"],
"properties": {
"id": {
"type": "string",
"description": "Zone Transfer Request identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"zone_id": {
"type": "string",
"description": "Zone identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"zone_name": {
"type": ["string","null"],
"description": "Zone Name for the Request",
"maxLength": 255,
"readOnly": true
},
"target_project_id": {
"type": ["string", "null"],
"description": "Tenant identifier",
"maxLength": 160
},
"project_id": {
"type": ["string", "null"],
"description": "Project identifier",
"maxLength": 36,
"immutable": true
},
"description": {
"type": ["string", "null"],
"description": "Description for the Request",
"maxLength": 160
},
"key": {
"type": "string",
"description": "Password used to complete the transfer",
"maxLength": 160
},
"status": {
"type": "string",
"description": "Zone Status",
"enum": ["ACTIVE", "PENDING", "DELETED", "ERROR", "COMPLETE"],
"readOnly": true
},
"created_at": {
"type": "string",
"description": "Date and time of Request creation",
"format": "date-time",
"readOnly": true
},
"updated_at": {
"type": ["string", "null"],
"description": "Date and time of last Request modification",
"format": "date-time",
"readOnly": true
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
}
}
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zone_transfer_request",
"title": "zone_transfer_request",
"description": "Zone Transfer Request",
"additionalProperties": false,
"required": ["transfer_requests"],
"properties": {
"transfer_request": {
"type": "array",
"description": "Zone Transfer Requests",
"items": {"$ref": "transfer_request#/properties/transfer_request"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": ["string", "null"],
"format": "url"
},
"previous": {
"type": ["string", "null"],
"format": "url"
}
}
}
}
}

View File

@ -179,7 +179,7 @@ class SQLAlchemy(object):
def _find(self, context, table, cls, list_cls, exc_notfound, criterion,
one=False, marker=None, limit=None, sort_key=None,
sort_dir=None, query=None):
sort_dir=None, query=None, apply_tenant_criteria=True):
sort_key = sort_key or 'created_at'
sort_dir = sort_dir or 'asc'
@ -187,7 +187,8 @@ class SQLAlchemy(object):
if query is None:
query = select([table])
query = self._apply_criterion(table, query, criterion)
query = self._apply_tenant_criteria(context, table, query)
if apply_tenant_criteria:
query = self._apply_tenant_criteria(context, table, query)
query = self._apply_deleted_criteria(context, table, query)
# Execute the Query

View File

@ -19,6 +19,7 @@ import hashlib
from oslo.config import cfg
from oslo.db import options
from sqlalchemy import select, distinct, func
from sqlalchemy.sql.expression import or_
from designate.openstack.common import log as logging
from designate import exceptions
@ -287,10 +288,29 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
def update_domain(self, context, domain):
# Don't handle recordsets for now
return self._update(
tenant_id_changed = False
if 'tenant_id' in domain.obj_what_changed():
tenant_id_changed = True
updated_domain = self._update(
context, tables.domains, domain, exceptions.DuplicateDomain,
exceptions.DomainNotFound, ['recordsets'])
if tenant_id_changed:
recordsets_query = tables.recordsets.update().\
where(tables.recordsets.c.domain_id == domain.id)\
.values({'tenant_id': domain.tenant_id})
records_query = tables.records.update().\
where(tables.records.c.domain_id == domain.id).\
values({'tenant_id': domain.tenant_id})
self.session.execute(records_query)
self.session.execute(recordsets_query)
return updated_domain
def delete_domain(self, context, domain_id):
# Fetch the existing domain, we'll need to return it.
domain = self._find_domains(context, {'id': domain_id}, one=True)
@ -735,6 +755,169 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
return deleted_pool_attribute
# Zone Transfer Methods
def _find_zone_transfer_requests(self, context, criterion, one=False,
marker=None, limit=None, sort_key=None,
sort_dir=None):
table = tables.zone_transfer_requests
ljoin = tables.zone_transfer_requests.join(
tables.domains,
tables.zone_transfer_requests.c.domain_id == tables.domains.c.id)
query = select(
[table, tables.domains.c.name.label("domain_name")]
).select_from(ljoin)
if context.all_tenants:
LOG.debug('Including all tenants items in query results')
else:
query = query.where(or_(
table.c.tenant_id == context.tenant,
table.c.target_tenant_id == context.tenant))
return self._find(
context, table, objects.ZoneTransferRequest,
objects.ZoneTransferRequestList,
exceptions.ZoneTransferRequestNotFound,
criterion,
one=one, marker=marker, limit=limit, sort_dir=sort_dir,
sort_key=sort_key, query=query, apply_tenant_criteria=False
)
def create_zone_transfer_request(self, context, zone_transfer_request):
try:
criterion = {"domain_id": zone_transfer_request.domain_id,
"status": "ACTIVE"}
self.find_zone_transfer_request(
context, criterion)
except exceptions.ZoneTransferRequestNotFound:
return self._create(
tables.zone_transfer_requests,
zone_transfer_request,
exceptions.DuplicateZoneTransferRequest)
else:
raise exceptions.DuplicateZoneTransferRequest()
def find_zone_transfer_requests(self, context, criterion=None,
marker=None, limit=None, sort_key=None,
sort_dir=None):
return self._find_zone_transfer_requests(
context, criterion, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir)
def get_zone_transfer_request(self, context, zone_transfer_request_id):
request = self._find_zone_transfer_requests(
context,
{'id': zone_transfer_request_id},
one=True)
return request
def find_zone_transfer_request(self, context, criterion):
return self._find_zone_transfer_requests(context, criterion, one=True)
def update_zone_transfer_request(self, context, zone_transfer_request):
zone_transfer_request.obj_reset_changes(('domain_name'))
updated_zt_request = self._update(
context,
tables.zone_transfer_requests,
zone_transfer_request,
exceptions.DuplicateZoneTransferRequest,
exceptions.ZoneTransferRequestNotFound,
skip_values=['domain_name'])
return updated_zt_request
def delete_zone_transfer_request(self, context, zone_transfer_request_id):
zone_transfer_request = self._find_zone_transfer_requests(
context,
{'id': zone_transfer_request_id},
one=True)
return self._delete(
context,
tables.zone_transfer_requests,
zone_transfer_request,
exceptions.ZoneTransferRequestNotFound)
def _find_zone_transfer_accept(self, context, criterion, one=False,
marker=None, limit=None, sort_key=None,
sort_dir=None):
return self._find(
context, tables.zone_transfer_accepts,
objects.ZoneTransferAccept,
objects.ZoneTransferAcceptList,
exceptions.ZoneTransferAcceptNotFound, criterion,
one, marker, limit, sort_key, sort_dir)
def _get_domain_name(self, context, domain_id, all_tenants=False):
if all_tenants:
ctxt = context.elevated()
ctxt.all_tenants = True
else:
ctxt = context
return self.get_domain(ctxt, domain_id).name
def create_zone_transfer_accept(self, context, zone_transfer_accept):
return self._create(
tables.zone_transfer_accepts,
zone_transfer_accept,
exceptions.DuplicateZoneTransferAccept)
def find_zone_transfer_accepts(self, context, criterion=None,
marker=None, limit=None, sort_key=None,
sort_dir=None):
return self._find_zone_transfer_accept(
context, criterion, marker=marker, limit=limit, sort_key=sort_key,
sort_dir=sort_dir)
def get_zone_transfer_accept(self, context, zone_transfer_accept_id):
return self._find_zone_transfer_accept(
context,
{'id': zone_transfer_accept_id},
one=True)
def find_zone_transfer_accept(self, context, criterion):
return self._find_zone_transfer_accept(
context,
criterion,
one=True)
def update_zone_transfer_accept(self, context, zone_transfer_accept):
return self._update(
context,
tables.zone_transfer_accepts,
zone_transfer_accept,
exceptions.DuplicateZoneTransferAccept,
exceptions.ZoneTransferAcceptNotFound)
def delete_zone_transfer_accept(self, context, zone_transfer_accept_id):
zone_transfer_accept = self._find_zone_transfer_accept(
context,
{'id': zone_transfer_accept_id},
one=True)
return self._delete(
context,
tables.zone_transfer_accepts,
zone_transfer_accept,
exceptions.ZoneTransferAcceptNotFound)
# diagnostics
def ping(self, context):
start_time = time.time()

View File

@ -0,0 +1,75 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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 sqlalchemy import Integer, String, DateTime, Enum, ForeignKey
from sqlalchemy.schema import Table, Column, MetaData
from designate.sqlalchemy.types import UUID
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
meta = MetaData()
zone_transfer_requests = Table(
'zone_transfer_requests',
meta,
Column('id', UUID(), primary_key=True),
Column('domain_id', UUID, ForeignKey('domains.id'), nullable=False),
Column('key', String(255), nullable=False),
Column('description', String(255), nullable=True),
Column('tenant_id', String(36), nullable=False),
Column('target_tenant_id', String(36), nullable=True),
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
nullable=False, server_default='ACTIVE',),
Column('created_at', DateTime()),
Column('updated_at', DateTime()),
Column('version', Integer(), nullable=False),
mysql_engine='INNODB',
mysql_charset='utf8'
)
zone_transfer_accepts = Table(
'zone_transfer_accepts',
meta,
Column('id', UUID(), primary_key=True),
Column('domain_id', UUID, ForeignKey('domains.id'), nullable=False),
Column('zone_transfer_request_id', UUID,
ForeignKey('zone_transfer_requests.id',
ondelete='CASCADE'),
nullable=False),
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
nullable=False, server_default='ACTIVE'),
Column('tenant_id', String(36), nullable=False),
Column('created_at', DateTime()),
Column('updated_at', DateTime()),
Column('version', Integer(), nullable=False),
mysql_engine='INNODB',
mysql_charset='utf8'
)
def upgrade(migrate_engine):
meta.bind = migrate_engine
Table('domains', meta, autoload=True)
zone_transfer_requests.create()
zone_transfer_accepts.create()
def downgrade(migrate_engine):
meta.bind = migrate_engine
zone_transfer_accepts.drop()
zone_transfer_requests.drop()

View File

@ -29,6 +29,7 @@ CONF = cfg.CONF
RESOURCE_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR']
RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR',
'SSHFP']
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
TSIG_ALGORITHMS = ['hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256',
'hmac-sha384', 'hmac-sha512']
POOL_PROVISIONERS = ['UNMANAGED']
@ -232,3 +233,47 @@ pool_attributes = Table('pool_attributes', metadata,
mysql_engine='INNODB',
mysql_charset='utf8'
)
zone_transfer_requests = Table('zone_transfer_requests', metadata,
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
Column('version', Integer(), default=1, nullable=False),
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
Column('domain_id', UUID, nullable=False),
Column("key", String(255), nullable=False),
Column("description", String(255), nullable=False),
Column("tenant_id", String(36), default=None, nullable=False),
Column("target_tenant_id", String(36), default=None, nullable=True),
Column("status", Enum(name='resource_statuses', *TASK_STATUSES),
nullable=False, server_default='ACTIVE',
default='ACTIVE'),
ForeignKeyConstraint(['domain_id'], ['domains.id'], ondelete='CASCADE'),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
zone_transfer_accepts = Table('zone_transfer_accepts', metadata,
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
Column('version', Integer(), default=1, nullable=False),
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
Column('domain_id', UUID, nullable=False),
Column('zone_transfer_request_id', UUID, nullable=False),
Column("tenant_id", String(36), default=None, nullable=False),
Column("status", Enum(name='resource_statuses', *TASK_STATUSES),
nullable=False, server_default='ACTIVE',
default='ACTIVE'),
ForeignKeyConstraint(['domain_id'], ['domains.id'], ondelete='CASCADE'),
ForeignKeyConstraint(
['zone_transfer_request_id'],
['zone_transfer_requests.id'],
ondelete='CASCADE'),
mysql_engine='InnoDB',
mysql_charset='utf8',
)

View File

@ -279,6 +279,13 @@ class TestCase(base.BaseTestCase):
['examplens1.org', 'examplens2.org']
]
zone_transfers_request_fixtures = [{
"description": "Test Transfer",
}, {
"description": "Test Transfer 2 - with target",
"target_tenant_id": "target_tenant_id"
}]
def setUp(self):
super(TestCase, self).setUp()
@ -525,6 +532,20 @@ class TestCase(base.BaseTestCase):
_values = copy.copy(self.name_server_fixtures[fixture])
return _values
def get_zone_transfer_request_fixture(self, fixture=0, values=None):
values = values or {}
_values = copy.copy(self.zone_transfers_request_fixtures[fixture])
_values.update(values)
return _values
def get_zone_transfer_accept_fixture(self, fixture=0, values=None):
values = values or {}
_values = copy.copy(self.zone_transfers_accept_fixtures[fixture])
_values.update(values)
return _values
def create_server(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)
@ -625,6 +646,43 @@ class TestCase(base.BaseTestCase):
return self.central_service.create_pool(
context, objects.Pool(**values))
def create_zone_transfer_request(self, domain, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)
values = self.get_zone_transfer_request_fixture(
fixture=fixture, values=kwargs)
if 'domain_id' not in values:
values['domain_id'] = domain.id
zone_transfer_request = objects.ZoneTransferRequest(**values)
return self.central_service.create_zone_transfer_request(
context, zone_transfer_request=zone_transfer_request)
def create_zone_transfer_accept(self, zone_transfer_request, **kwargs):
context = kwargs.pop('context', self.admin_context)
values = {}
if 'tenant_id' not in values:
values['tenant_id'] = context.tenant
if 'zone_transfer_request_id' not in values:
values['zone_transfer_request_id'] = zone_transfer_request.id
if 'domain_id' not in values:
values['domain_id'] = zone_transfer_request.domain_id
if 'key' not in values:
values['key'] = zone_transfer_request.key
zone_transfer_accept = objects.ZoneTransferAccept(**values)
return self.central_service.create_zone_transfer_accept(
context, zone_transfer_accept)
def _ensure_interface(self, interface, implementation):
for name in interface.__abstractmethods__:
in_arginfo = inspect.getargspec(getattr(interface, name))

View File

@ -0,0 +1,181 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Graham Hayes <graham.hayes@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.tests.test_api.test_v2 import ApiV2TestCase
class ApiV2ZoneTransfersTest(ApiV2TestCase):
def setUp(self):
super(ApiV2ZoneTransfersTest, self).setUp()
self.domain = self.create_domain()
self.tenant_1_context = self.get_context(tenant=1)
self.tenant_2_context = self.get_context(tenant=2)
def test_create_zone_transfer_request(self):
response = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request': {}})
# 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('transfer_request', response.json)
self.assertIn('links', response.json['transfer_request'])
self.assertIn('self', response.json['transfer_request']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['transfer_request'])
self.assertIn('created_at', response.json['transfer_request'])
self.assertEqual('ACTIVE', response.json['transfer_request']['status'])
self.assertEqual(
self.domain.id,
response.json['transfer_request']['zone_id'])
self.assertIsNone(response.json['transfer_request']['updated_at'])
def test_create_zone_transfer_request_scoped(self):
response = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request':
{'target_project_id': str(self.tenant_1_context.tenant)}})
# 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('transfer_request', response.json)
self.assertIn('links', response.json['transfer_request'])
self.assertIn('self', response.json['transfer_request']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['transfer_request'])
self.assertIn('created_at', response.json['transfer_request'])
self.assertEqual('ACTIVE', response.json['transfer_request']['status'])
self.assertEqual(
str(self.tenant_1_context.tenant),
response.json['transfer_request']['target_project_id'])
self.assertEqual(
self.domain.id,
response.json['transfer_request']['zone_id'])
self.assertIsNone(response.json['transfer_request']['updated_at'])
def test_get_zone_transfer_request(self):
initial = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request': {}})
response = self.client.get(
'/zones/tasks/transfer_requests/%s' %
(initial.json['transfer_request']['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('transfer_request', response.json)
self.assertIn('links', response.json['transfer_request'])
self.assertIn('self', response.json['transfer_request']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['transfer_request'])
self.assertIn('created_at', response.json['transfer_request'])
self.assertEqual('ACTIVE', response.json['transfer_request']['status'])
self.assertEqual(
self.domain.id,
response.json['transfer_request']['zone_id'])
self.assertIn('updated_at', response.json['transfer_request'])
def test_update_zone_transfer_request(self):
initial = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request': {}})
response = self.client.patch_json(
'/zones/tasks/transfer_requests/%s' %
(initial.json['transfer_request']['id']),
{'transfer_request': {"description": "TEST"}})
# 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('transfer_request', response.json)
self.assertIn('links', response.json['transfer_request'])
self.assertIn('self', response.json['transfer_request']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['transfer_request'])
self.assertIn('created_at', response.json['transfer_request'])
self.assertEqual('ACTIVE', response.json['transfer_request']['status'])
self.assertEqual(
self.domain.id,
response.json['transfer_request']['zone_id'])
self.assertEqual(
'TEST', response.json['transfer_request']['description'])
self.assertIn('updated_at', response.json['transfer_request'])
def test_delete_zone_transfer_request(self):
initial = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request': {}})
response = self.client.delete(
'/zones/tasks/transfer_requests/%s' %
(initial.json['transfer_request']['id']))
# Check the headers are what we expect
self.assertEqual(204, response.status_int)
def test_create_zone_transfer_accept(self):
initial = self.client.post_json(
'/zones/%s/tasks/transfer_requests' % (self.domain.id),
{'transfer_request': {}})
response = self.client.post_json(
'/zones/tasks/transfer_accepts',
{'transfer_accept': {
'zone_transfer_request_id':
initial.json['transfer_request']['id'],
'key': initial.json['transfer_request']['key']
}})
new_ztr = self.client.get(
'/zones/tasks/transfer_requests/%s' %
(initial.json['transfer_request']['id']))
# 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('transfer_accept', response.json)
self.assertIn('links', response.json['transfer_accept'])
self.assertIn('self', response.json['transfer_accept']['links'])
self.assertIn('zone', response.json['transfer_accept']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['transfer_accept'])
self.assertEqual(
'COMPLETE',
response.json['transfer_accept']['status'])
self.assertEqual(
'COMPLETE',
new_ztr.json['transfer_request']['status'])

View File

@ -2473,3 +2473,246 @@ class CentralServiceTest(CentralTestCase):
# Verify that the pool has been deleted
with testtools.ExpectedException(exceptions.PoolNotFound):
self.central_service.get_pool(self.admin_context, pool['id'])
def test_create_zone_transfer_request(self):
domain = self.create_domain()
zone_transfer_request = self.create_zone_transfer_request(domain)
# Verify all values have been set correctly
self.assertIsNotNone(zone_transfer_request.id)
self.assertIsNotNone(zone_transfer_request.tenant_id)
self.assertIsNotNone(zone_transfer_request.key)
self.assertEqual(zone_transfer_request.domain_id, domain.id)
def test_create_zone_transfer_request_duplicate(self):
domain = self.create_domain()
self.create_zone_transfer_request(domain)
with testtools.ExpectedException(
exceptions.DuplicateZoneTransferRequest):
self.create_zone_transfer_request(domain)
def test_create_scoped_zone_transfer_request(self):
domain = self.create_domain()
values = self.get_zone_transfer_request_fixture(fixture=1)
zone_transfer_request = self.create_zone_transfer_request(domain,
fixture=1)
# Verify all values have been set correctly
self.assertIsNotNone(zone_transfer_request.id)
self.assertIsNotNone(zone_transfer_request.tenant_id)
self.assertEqual(zone_transfer_request.domain_id, domain.id)
self.assertIsNotNone(zone_transfer_request.key)
self.assertEqual(
zone_transfer_request.target_tenant_id,
values['target_tenant_id'])
def test_get_zone_transfer_request(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain,
fixture=1)
retrived_zt = self.central_service.get_zone_transfer_request(
self.admin_context,
zt_request.id)
self.assertEqual(zt_request.domain_id, retrived_zt.domain_id)
self.assertEqual(zt_request.key, retrived_zt.key)
def test_get_zone_transfer_request_scoped(self):
tenant_1_context = self.get_context(tenant=1)
tenant_2_context = self.get_context(tenant=2)
tenant_3_context = self.get_context(tenant=3)
domain = self.create_domain(context=tenant_1_context)
zt_request = self.create_zone_transfer_request(
domain,
context=tenant_1_context,
target_tenant_id=2)
self.central_service.get_zone_transfer_request(
tenant_2_context, zt_request.id)
self.central_service.get_zone_transfer_request(
tenant_1_context, zt_request.id)
with testtools.ExpectedException(exceptions.Forbidden):
self.central_service.get_zone_transfer_request(
tenant_3_context, zt_request.id)
def test_update_zone_transfer_request(self):
domain = self.create_domain()
zone_transfer_request = self.create_zone_transfer_request(domain)
zone_transfer_request.description = 'TEST'
self.central_service.update_zone_transfer_request(
self.admin_context, zone_transfer_request)
# Verify all values have been set correctly
self.assertIsNotNone(zone_transfer_request.id)
self.assertIsNotNone(zone_transfer_request.tenant_id)
self.assertIsNotNone(zone_transfer_request.key)
self.assertEqual(zone_transfer_request.description, 'TEST')
def test_delete_zone_transfer_request(self):
domain = self.create_domain()
zone_transfer_request = self.create_zone_transfer_request(domain)
self.central_service.delete_zone_transfer_request(
self.admin_context, zone_transfer_request.id)
with testtools.ExpectedException(
exceptions.ZoneTransferRequestNotFound):
self.central_service.get_zone_transfer_request(
self.admin_context,
zone_transfer_request.id)
def test_create_zone_transfer_accept(self):
tenant_1_context = self.get_context(tenant=1)
tenant_2_context = self.get_context(tenant=2)
admin_context = self.get_admin_context()
admin_context.all_tenants = True
domain = self.create_domain(context=tenant_1_context)
recordset = self.create_recordset(domain, context=tenant_1_context)
record = self.create_record(
domain, recordset, context=tenant_1_context)
zone_transfer_request = self.create_zone_transfer_request(
domain, context=tenant_1_context)
zone_transfer_accept = objects.ZoneTransferAccept()
zone_transfer_accept.zone_transfer_request_id =\
zone_transfer_request.id
zone_transfer_accept.key = zone_transfer_request.key
zone_transfer_accept.domain_id = domain.id
zone_transfer_accept = \
self.central_service.create_zone_transfer_accept(
tenant_2_context, zone_transfer_accept)
result = {}
result['domain'] = self.central_service.get_domain(
admin_context, domain.id)
result['recordset'] = self.central_service.get_recordset(
admin_context, domain.id, recordset.id)
result['record'] = self.central_service.get_record(
admin_context, domain.id, recordset.id, record.id)
result['zt_accept'] = self.central_service.get_zone_transfer_accept(
admin_context, zone_transfer_accept.id)
result['zt_request'] = self.central_service.get_zone_transfer_request(
admin_context, zone_transfer_request.id)
self.assertEqual(
result['domain'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['recordset'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['record'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['zt_accept'].status, 'COMPLETE')
self.assertEqual(
result['zt_request'].status, 'COMPLETE')
def test_create_zone_transfer_accept_scoped(self):
tenant_1_context = self.get_context(tenant=1)
tenant_2_context = self.get_context(tenant=2)
admin_context = self.get_admin_context()
admin_context.all_tenants = True
domain = self.create_domain(context=tenant_1_context)
recordset = self.create_recordset(domain, context=tenant_1_context)
record = self.create_record(
domain, recordset, context=tenant_1_context)
zone_transfer_request = self.create_zone_transfer_request(
domain,
context=tenant_1_context,
target_tenant_id='2')
zone_transfer_accept = objects.ZoneTransferAccept()
zone_transfer_accept.zone_transfer_request_id =\
zone_transfer_request.id
zone_transfer_accept.key = zone_transfer_request.key
zone_transfer_accept.domain_id = domain.id
zone_transfer_accept = \
self.central_service.create_zone_transfer_accept(
tenant_2_context, zone_transfer_accept)
result = {}
result['domain'] = self.central_service.get_domain(
admin_context, domain.id)
result['recordset'] = self.central_service.get_recordset(
admin_context, domain.id, recordset.id)
result['record'] = self.central_service.get_record(
admin_context, domain.id, recordset.id, record.id)
result['zt_accept'] = self.central_service.get_zone_transfer_accept(
admin_context, zone_transfer_accept.id)
result['zt_request'] = self.central_service.get_zone_transfer_request(
admin_context, zone_transfer_request.id)
self.assertEqual(
result['domain'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['recordset'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['record'].tenant_id, str(tenant_2_context.tenant))
self.assertEqual(
result['zt_accept'].status, 'COMPLETE')
self.assertEqual(
result['zt_request'].status, 'COMPLETE')
def test_create_zone_transfer_accept_failed_key(self):
tenant_1_context = self.get_context(tenant=1)
tenant_2_context = self.get_context(tenant=2)
admin_context = self.get_admin_context()
admin_context.all_tenants = True
domain = self.create_domain(context=tenant_1_context)
zone_transfer_request = self.create_zone_transfer_request(
domain,
context=tenant_1_context,
target_tenant_id=2)
zone_transfer_accept = objects.ZoneTransferAccept()
zone_transfer_accept.zone_transfer_request_id =\
zone_transfer_request.id
zone_transfer_accept.key = 'WRONG KEY'
zone_transfer_accept.domain_id = domain.id
with testtools.ExpectedException(exceptions.IncorrectZoneTransferKey):
zone_transfer_accept = \
self.central_service.create_zone_transfer_accept(
tenant_2_context, zone_transfer_accept)
def test_create_zone_tarnsfer_accept_out_of_tenant_scope(self):
tenant_1_context = self.get_context(tenant=1)
tenant_3_context = self.get_context(tenant=3)
admin_context = self.get_admin_context()
admin_context.all_tenants = True
domain = self.create_domain(context=tenant_1_context)
zone_transfer_request = self.create_zone_transfer_request(
domain,
context=tenant_1_context,
target_tenant_id=2)
zone_transfer_accept = objects.ZoneTransferAccept()
zone_transfer_accept.zone_transfer_request_id =\
zone_transfer_request.id
zone_transfer_accept.key = zone_transfer_request.key
zone_transfer_accept.domain_id = domain.id
with testtools.ExpectedException(exceptions.Forbidden):
zone_transfer_accept = \
self.central_service.create_zone_transfer_accept(
tenant_3_context, zone_transfer_accept)

View File

@ -1986,3 +1986,164 @@ class StorageTestCase(object):
with testtools.ExpectedException(exceptions.PoolNotFound):
uuid = '203ca44f-c7e7-4337-9a02-0d735833e6aa'
self.storage.delete_pool(self.admin_context, uuid)
def test_create_zone_transfer_request(self):
domain = self.create_domain()
values = {
'tenant_id': self.admin_context.tenant,
'domain_id': domain.id,
'key': 'qwertyuiop'
}
result = self.storage.create_zone_transfer_request(
self.admin_context, objects.ZoneTransferRequest(**values))
self.assertEqual(result['tenant_id'], self.admin_context.tenant)
self.assertIn('status', result)
def test_create_zone_transfer_request_scoped(self):
domain = self.create_domain()
tenant_2_context = self.get_context(tenant='2')
tenant_3_context = self.get_context(tenant='3')
values = {
'tenant_id': self.admin_context.tenant,
'domain_id': domain.id,
'key': 'qwertyuiop',
'target_tenant_id': tenant_2_context.tenant,
}
result = self.storage.create_zone_transfer_request(
self.admin_context, objects.ZoneTransferRequest(**values))
self.assertIsNotNone(result['id'])
self.assertIsNotNone(result['created_at'])
self.assertIsNone(result['updated_at'])
self.assertEqual(result['tenant_id'], self.admin_context.tenant)
self.assertEqual(result['target_tenant_id'], tenant_2_context.tenant)
self.assertIn('status', result)
stored_ztr = self.storage.get_zone_transfer_request(
tenant_2_context, result.id)
self.assertEqual(stored_ztr['tenant_id'], self.admin_context.tenant)
self.assertEqual(result['id'], stored_ztr['id'])
with testtools.ExpectedException(
exceptions.ZoneTransferRequestNotFound):
self.storage.get_zone_transfer_request(
tenant_3_context, result.id)
def test_delete_zone_transfer_request(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
self.storage.delete_zone_transfer_request(
self.admin_context, zt_request.id)
with testtools.ExpectedException(
exceptions.ZoneTransferRequestNotFound):
self.storage.get_zone_transfer_request(
self.admin_context, zt_request.id)
def test_update_zone_transfer_request(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
zt_request.description = 'New description'
result = self.storage.update_zone_transfer_request(
self.admin_context, zt_request)
self.assertEqual(result.description, 'New description')
def test_get_zone_transfer_request(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
result = self.storage.get_zone_transfer_request(
self.admin_context, zt_request.id)
self.assertEqual(result.id, zt_request.id)
self.assertEqual(result.domain_id, zt_request.domain_id)
def test_create_zone_transfer_accept(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
values = {
'tenant_id': self.admin_context.tenant,
'zone_transfer_request_id': zt_request.id,
'domain_id': domain.id,
'key': zt_request.key
}
result = self.storage.create_zone_transfer_accept(
self.admin_context, objects.ZoneTransferAccept(**values))
self.assertIsNotNone(result['id'])
self.assertIsNotNone(result['created_at'])
self.assertIsNone(result['updated_at'])
self.assertEqual(result['tenant_id'], self.admin_context.tenant)
self.assertIn('status', result)
def test_transfer_zone_ownership(self):
tenant_1_context = self.get_context(tenant='1')
tenant_2_context = self.get_context(tenant='2')
admin_context = self.get_admin_context()
admin_context.all_tenants = True
domain = self.create_domain(context=tenant_1_context)
recordset = self.create_recordset(domain, context=tenant_1_context)
record = self.create_record(
domain, recordset, context=tenant_1_context)
updated_domain = domain
updated_domain.tenant_id = tenant_2_context.tenant
self.storage.update_domain(
admin_context, updated_domain)
saved_domain = self.storage.get_domain(
admin_context, domain.id)
saved_recordset = self.storage.get_recordset(
admin_context, recordset.id)
saved_record = self.storage.get_record(
admin_context, record.id)
self.assertEqual(saved_domain.tenant_id, tenant_2_context.tenant)
self.assertEqual(saved_recordset.tenant_id, tenant_2_context.tenant)
self.assertEqual(saved_record.tenant_id, tenant_2_context.tenant)
def test_delete_zone_transfer_accept(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
zt_accept = self.create_zone_transfer_accept(zt_request)
self.storage.delete_zone_transfer_accept(
self.admin_context, zt_accept.id)
with testtools.ExpectedException(
exceptions.ZoneTransferAcceptNotFound):
self.storage.get_zone_transfer_accept(
self.admin_context, zt_accept.id)
def test_update_zone_transfer_accept(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
zt_accept = self.create_zone_transfer_accept(zt_request)
zt_accept.status = 'COMPLETE'
result = self.storage.update_zone_transfer_accept(
self.admin_context, zt_accept)
self.assertEqual(result.status, 'COMPLETE')
def test_get_zone_transfer_accept(self):
domain = self.create_domain()
zt_request = self.create_zone_transfer_request(domain)
zt_accept = self.create_zone_transfer_accept(zt_request)
result = self.storage.get_zone_transfer_accept(
self.admin_context, zt_accept.id)
self.assertEqual(result.id, zt_accept.id)
self.assertEqual(result.domain_id, zt_accept.domain_id)

View File

@ -15,9 +15,9 @@
.. _devstack:
========================
========
DevStack
========================
========
The Designate team maintains a fork of devstack with Designate integration.
@ -135,3 +135,4 @@ Instructions
| type | A |
| domain_id | 1fb5d17c-efaf-4e3c-aac0-482875d24b3e |
+------------+--------------------------------------+

View File

@ -285,7 +285,7 @@ Import Zone
.. http:post:: /zones
To import a zonefile, set the Content-type to **text/dns**. The
To import a zonefile, set the Content-type to **text/dns** . The
**zoneextractor.py** tool in the **contrib** folder can generate zonefiles
that are suitable for Designate (without any **$INCLUDE** statements for
example).
@ -379,3 +379,213 @@ Export Zone
:statuscode 406: Not Acceptable
Notice how the SOA and NS records are replaced with the Designate server(s).
Transfer Zone
-------------
Create Zone Transfer Request
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. http:post:: /zones/(uuid:id)/tasks/transfer_requests
To initiate a transfer the original owner must create a transfer request.
This will return two items that are required to continue:
* key: a password that is used to validate the transfer
* id: ID of the request.
Both of these should be communicated out of band (email / IM / etc) to the intended recipient
There is an option of limiting the transfer to a single project. If that is required, the person initiating the transfer
will need the Project ID. This will also allow the targeted project to see the transfer in their list of requests.
A non-targeted request will not show in a list operation, apart from the owning projects request.
An targeted request will only show in the targets and owners lists.
An un-targeted request can be viewed by any authenticated user.
**Example Request**
.. sourcecode:: http
POST /v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff8/tasks/transfer_requests HTTP/1.1
Host: 127.0.0.1:9001
Accept: application/json
Content-Type: application/json
{
"transfer_request":{
"target_project_id": "123456",
"description": "Transfer qa.dev.example.com. to QA Team"
}
}
**Example Response**
.. sourcecode:: http
HTTP/1.1 201 Created
Content-Type: application/json
{
"transfer_request": {
"created_at": "2014-07-17T20:34:40.882579",
"description": null,
"id": "f2ad17b5-807a-423f-a991-e06236c247be",
"key": "9Z2R50Y0",
"project_id": "1",
"status": "ACTIVE",
"target_project_id": "123456",
"updated_at": null,
"zone_id": "6b78734a-aef1-45cd-9708-8eb3c2d26ff8",
"zone_name": "qa.dev.example.com.",
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_requests/f2ad17b5-807a-423f-a991-e06236c247be"
}
}
}
:form description: UTF-8 text field
:form target_project_id: Optional field to only allow a single tenant to accept the transfer request
List Zone Transfer Requests
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. http:get:: /zones/tasks/transfer_requests
List all transfer requests that the requesting project have created, or are targeted to that project
The detail shown will differ, based on who the requester is.
**Example Request**
.. sourcecode:: http
GET /zones/tasks/transfer_requests HTTP/1.1
Host: 127.0.0.1:9001
Accept: application/json
**Example Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"transfer_requests": [
{
"created_at": "2014-07-17T20:34:40.882579",
"description": "This was created by the requesting project",
"id": "f2ad17b5-807a-423f-a991-e06236c247be",
"key": "9Z2R50Y0",
"project_id": "1",
"status": "ACTIVE",
"target_project_id": "123456",
"updated_at": null,
"zone_id": "6b78734a-aef1-45cd-9708-8eb3c2d26ff8",
"zone_name": "qa.dev.example.com.",
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_requests/f2ad17b5-807a-423f-a991-e06236c247be"
}
},
{
"description": "This is scoped to the requesting project",
"id": "efd2d720-b0c4-43d4-99f7-d9b53e08860d",
"zone_id": "2c4d5e37-f823-4bee-9859-031cb44f80e7",
"zone_name": "subdomain.example.com.",
"status": "ACTIVE",
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_requests/efd2d720-b0c4-43d4-99f7-d9b53e08860d"
}
}
],
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_requests"
}
}
View a Transfer Request
^^^^^^^^^^^^^^^^^^^^^^^
.. http:get:: /zones/tasks/transfer_requests/(uuid:id)
Show details about a request.
This allows a user to view a transfer request before accepting it
**Example Request**
.. sourcecode:: http
GET /v2/zones/tasks/transfer_requests/f2ad17b5-807a-423f-a991-e06236c247be HTTP/1.1
Host: 127.0.0.1:9001
Accept: application/json
**Example Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"transfer_request":{
"description": "This is scoped to the requesting project",
"id": "efd2d720-b0c4-43d4-99f7-d9b53e08860d",
"zone_id": "2c4d5e37-f823-4bee-9859-031cb44f80e7",
"zone_name": "subdomain.example.com.",
"status": "ACTIVE",
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_requests/efd2d720-b0c4-43d4-99f7-d9b53e08860d"
}
}
}
Accept a Transfer Request
^^^^^^^^^^^^^^^^^^^^^^^^^
.. http:post:: /zones/tasks/transfer_accepts
Accept a zone transfer request. This is called by the project that will own the zone
(i.e. the project that will maintain the zone)
Once the API returns "Complete" the zone has been transfered to the new project
**Example Request**
.. sourcecode:: http
POST /v2/zones/tasks/transfer_accept HTTP/1.1
Host: 127.0.0.1:9001
Accept: application/json
Content-Type: application/json
{
"transfer_accept":{
"key":"9Z2R50Y0",
"zone_transfer_request_id":"f2ad17b5-807a-423f-a991-e06236c247be"
}
}
**Example Response**
.. sourcecode:: http
HTTP/1.1 201 Created
Content-Type: application/json
{
"transfer_accept": {
"id": "581891d5-99f5-49e1-86c3-eec0f44d66fd",
"links": {
"self": "http://127.0.0.1:9001/v2/zones/tasks/transfer_accepts/581891d5-99f5-49e1-86c3-eec0f44d66fd",
"zone": "http://127.0.0.1:9001/v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff8"
},
"status": "COMPLETE"
}
}

View File

@ -2,6 +2,10 @@
"admin": "role:admin or is_admin:True",
"owner": "tenant:%(tenant_id)s",
"admin_or_owner": "rule:admin or rule:owner",
"target": "tenant:%(target_tenant_id)s",
"owner_or_target":"rule:target or rule:owner",
"admin_or_owner_or_target":"rule:owner_or_target or rule:admin",
"admin_or_target":"rule:admin or rule:target",
"default": "rule:admin_or_owner",
@ -76,5 +80,20 @@
"diagnostics_ping": "rule:admin",
"diagnostics_sync_domains": "rule:admin",
"diagnostics_sync_domain": "rule:admin",
"diagnostics_sync_record": "rule:admin"
"diagnostics_sync_record": "rule:admin",
"create_zone_transfer_request": "rule:admin_or_owner",
"get_zone_transfer_request": "rule:admin_or_owner or tenant:%(target_tenant_id)s or None:%(target_tenant_id)s",
"get_zone_transfer_request_detailed": "rule:admin_or_owner",
"find_zone_transfer_requests": "@",
"find_zone_transfer_request": "@",
"update_zone_transfer_request": "rule:admin_or_owner",
"delete_zone_transfer_request": "rule:admin_or_owner",
"create_zone_transfer_accept": "rule:admin_or_owner or tenant:%(target_tenant_id)s or None:%(target_tenant_id)s",
"get_zone_transfer_accept": "rule:admin_or_owner",
"find_zone_transfer_accepts": "rule:admin",
"find_zone_transfer_accept": "rule:admin",
"update_zone_transfer_accept": "rule:admin",
"delete_zone_transfer_accept": "rule:admin"
}