Asynchronous Zone Import
* Creates /v2/zones/tasks/imports, which allows users to view imports as resources * Creates new database table zone_tasks for asynchronous tasks related to zones, along with the associated objects/adapters * Imports are done by passing over the request body, creating an async record in the db, and spawning a thread to do the import * Adds a config option to enable zone import Implements: async-import-export APIImpact: Adds /zones/tasks/imports and removes import from admin api Change-Id: Ib23810bf8b25d962b9d2d75e042bb097f3c12f7a
This commit is contained in:
parent
53a7300103
commit
021946e386
@ -1,82 +0,0 @@
|
||||
# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from dns import zone as dnszone
|
||||
from dns import exception as dnsexception
|
||||
import pecan
|
||||
from oslo_log import log as logging
|
||||
from oslo_config import cfg
|
||||
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate import dnsutils
|
||||
from designate import exceptions
|
||||
from designate.objects.adapters import DesignateAdapter
|
||||
from designate import policy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImportController(rest.RestController):
|
||||
|
||||
BASE_URI = cfg.CONF['service:api'].api_base_uri.rstrip('/')
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def post_all(self):
|
||||
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = pecan.request.environ['context']
|
||||
|
||||
policy.check('zone_import', context)
|
||||
|
||||
if request.content_type != 'text/dns':
|
||||
raise exceptions.UnsupportedContentType(
|
||||
'Content-type must be text/dns')
|
||||
|
||||
try:
|
||||
dnspython_zone = dnszone.from_text(
|
||||
request.body,
|
||||
# Don't relativize, otherwise we end up with '@' record names.
|
||||
relativize=False,
|
||||
# Dont check origin, we allow missing NS records (missing SOA
|
||||
# records are taken care of in _create_zone).
|
||||
check_origin=False)
|
||||
domain = dnsutils.from_dnspython_zone(dnspython_zone)
|
||||
domain.type = 'PRIMARY'
|
||||
|
||||
for rrset in list(domain.recordsets):
|
||||
if rrset.type in ('NS', 'SOA'):
|
||||
domain.recordsets.remove(rrset)
|
||||
|
||||
except dnszone.UnknownOrigin:
|
||||
raise exceptions.BadRequest('The $ORIGIN statement is required and'
|
||||
' must be the first statement in the'
|
||||
' zonefile.')
|
||||
except dnsexception.SyntaxError:
|
||||
raise exceptions.BadRequest('Malformed zonefile.')
|
||||
|
||||
zone = self.central_api.create_domain(context, domain)
|
||||
|
||||
if zone['status'] == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 201
|
||||
|
||||
zone = DesignateAdapter.render('API_v2', zone, request=request)
|
||||
|
||||
zone['links']['self'] = '%s/%s/%s' % (
|
||||
self.BASE_URI, 'v2/zones', zone['id'])
|
||||
|
||||
response.headers['Location'] = zone['links']['self']
|
||||
|
||||
return zone
|
@ -15,7 +15,6 @@
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.api.admin.controllers.extensions import import_
|
||||
from designate.api.admin.controllers.extensions import export
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -28,12 +27,6 @@ class ZonesController(rest.RestController):
|
||||
return '.zones'
|
||||
|
||||
def __init__(self):
|
||||
# Import is a keyword - so we have to do a setattr instead
|
||||
setattr(self, 'import', import_.ImportController())
|
||||
super(ZonesController, self).__init__()
|
||||
|
||||
# We cannot do an assignment as import is a keyword. it is done as part of
|
||||
# the __init__() above
|
||||
#
|
||||
# import = import_.CountsController()
|
||||
export = export.ExportController()
|
||||
|
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from oslo_log import log as logging
|
||||
from oslo_config import cfg
|
||||
|
||||
from designate.api.v2.controllers.zones.tasks.transfer_requests \
|
||||
import TransferRequestsController as TRC
|
||||
@ -21,7 +22,10 @@ from designate.api.v2.controllers.zones.tasks.transfer_accepts \
|
||||
import TransferAcceptsController as TRA
|
||||
from designate.api.v2.controllers.zones.tasks import abandon
|
||||
from designate.api.v2.controllers.zones.tasks.xfr import XfrController
|
||||
from designate.api.v2.controllers.zones.tasks.imports \
|
||||
import ZoneImportController
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -31,3 +35,4 @@ class TasksController(object):
|
||||
transfer_requests = TRC()
|
||||
abandon = abandon.AbandonController()
|
||||
xfr = XfrController()
|
||||
imports = ZoneImportController()
|
||||
|
102
designate/api/v2/controllers/zones/tasks/imports.py
Normal file
102
designate/api/v2/controllers/zones/tasks/imports.py
Normal file
@ -0,0 +1,102 @@
|
||||
# Copyright 2015 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspae.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 rest
|
||||
from designate.objects.adapters.api_v2.zone_import \
|
||||
import ZoneImportAPIv2Adapter
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZoneImportController(rest.RestController):
|
||||
|
||||
SORT_KEYS = ['created_at', 'id', 'updated_at']
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('import_id')
|
||||
def get_one(self, import_id):
|
||||
"""Get imports"""
|
||||
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
|
||||
return ZoneImportAPIv2Adapter.render(
|
||||
'API_v2',
|
||||
self.central_api.get_zone_import(
|
||||
context, import_id),
|
||||
request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def get_all(self, **params):
|
||||
"""List ZoneImports"""
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
marker, limit, sort_key, sort_dir = utils.get_paging_params(
|
||||
params, self.SORT_KEYS)
|
||||
|
||||
# Extract any filter params.
|
||||
accepted_filters = ('status', 'message', 'zone_id', )
|
||||
|
||||
criterion = self._apply_filter_params(
|
||||
params, accepted_filters, {})
|
||||
|
||||
return ZoneImportAPIv2Adapter.render(
|
||||
'API_v2',
|
||||
self.central_api.find_zone_imports(
|
||||
context, criterion, marker, limit, sort_key, sort_dir),
|
||||
request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def post_all(self):
|
||||
"""Create ZoneImport"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
body = request.body
|
||||
|
||||
if request.content_type != 'text/dns':
|
||||
raise exceptions.UnsupportedContentType(
|
||||
'Content-type must be text/dns')
|
||||
|
||||
# Create the zone_import
|
||||
zone_import = self.central_api.create_zone_import(
|
||||
context, body)
|
||||
response.status_int = 202
|
||||
|
||||
zone_import = ZoneImportAPIv2Adapter.render(
|
||||
'API_v2', zone_import, request=request)
|
||||
|
||||
response.headers['Location'] = zone_import['links']['self']
|
||||
# Prepare and return the response body
|
||||
return zone_import
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_import_id')
|
||||
def delete_one(self, zone_import_id):
|
||||
"""Delete Zone"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
self.central_api.delete_zone_import(context, zone_import_id)
|
||||
response.status_int = 204
|
||||
|
||||
return ''
|
@ -48,14 +48,15 @@ class CentralAPI(object):
|
||||
4.3 - Added Zone Transfer Methods
|
||||
5.0 - Remove dead server code
|
||||
5.1 - Add xfr_domain
|
||||
5.2 - Add Zone Import methods
|
||||
"""
|
||||
RPC_API_VERSION = '5.1'
|
||||
RPC_API_VERSION = '5.2'
|
||||
|
||||
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='5.1')
|
||||
self.client = rpc.get_client(target, version_cap='5.2')
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
@ -494,5 +495,38 @@ class CentralAPI(object):
|
||||
|
||||
def xfr_domain(self, context, domain_id):
|
||||
LOG.info(_LI("xfr_domain: Calling central's xfr_domain"))
|
||||
cctxt = self.client.prepare(version='5.1')
|
||||
cctxt = self.client.prepare(version='5.2')
|
||||
return cctxt.call(context, 'xfr_domain', domain_id=domain_id)
|
||||
|
||||
# Zone Import Methods
|
||||
def create_zone_import(self, context, request_body):
|
||||
LOG.info(_LI("create_zone_import: Calling central's "
|
||||
"create_zone_import."))
|
||||
return self.client.call(context, 'create_zone_import',
|
||||
request_body=request_body)
|
||||
|
||||
def find_zone_imports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
LOG.info(_LI("find_zone_imports: Calling central's "
|
||||
"find_zone_imports."))
|
||||
return self.client.call(context, 'find_zone_imports',
|
||||
criterion=criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
def get_zone_import(self, context, zone_import_id):
|
||||
LOG.info(_LI("get_zone_import: Calling central's get_zone_import."))
|
||||
return self.client.call(context, 'get_zone_import',
|
||||
zone_import_id=zone_import_id)
|
||||
|
||||
def update_zone_import(self, context, zone_import):
|
||||
LOG.info(_LI("update_zone_import: Calling central's "
|
||||
"update_zone_import."))
|
||||
return self.client.call(context, 'update_zone_import',
|
||||
zone_import=zone_import)
|
||||
|
||||
def delete_zone_import(self, context, zone_import_id):
|
||||
LOG.info(_LI("delete_zone_import: Calling central's "
|
||||
"delete_zone_import."))
|
||||
return self.client.call(context, 'delete_zone_import',
|
||||
zone_import_id=zone_import_id)
|
||||
|
@ -24,6 +24,9 @@ import string
|
||||
import random
|
||||
import time
|
||||
|
||||
from eventlet import tpool
|
||||
from dns import zone as dnszone
|
||||
from dns import exception as dnsexception
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging as messaging
|
||||
from oslo_log import log as logging
|
||||
@ -36,6 +39,7 @@ from designate.i18n import _LC
|
||||
from designate.i18n import _LW
|
||||
from designate import context as dcontext
|
||||
from designate import exceptions
|
||||
from designate import dnsutils
|
||||
from designate import network_api
|
||||
from designate import objects
|
||||
from designate import policy
|
||||
@ -247,7 +251,7 @@ def notification(notification_type):
|
||||
|
||||
|
||||
class Service(service.RPCService, service.Service):
|
||||
RPC_API_VERSION = '5.1'
|
||||
RPC_API_VERSION = '5.2'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -865,6 +869,9 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
if domain.obj_attr_is_set('recordsets'):
|
||||
for rrset in domain.recordsets:
|
||||
# This allows eventlet to yield, as this looping operation
|
||||
# can be very long-lived.
|
||||
time.sleep(0)
|
||||
self._create_recordset_in_storage(
|
||||
context, domain, rrset, increment_serial=False)
|
||||
|
||||
@ -2452,3 +2459,126 @@ class Service(service.RPCService, service.Service):
|
||||
return self.storage.delete_zone_transfer_accept(
|
||||
context,
|
||||
zone_transfer_accept_id)
|
||||
|
||||
# Zone Import Methods
|
||||
@notification('dns.zone_import.create')
|
||||
def create_zone_import(self, context, request_body):
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('create_zone_import', context, target)
|
||||
|
||||
values = {
|
||||
'status': 'PENDING',
|
||||
'message': None,
|
||||
'domain_id': None,
|
||||
'tenant_id': context.tenant,
|
||||
'task_type': 'IMPORT'
|
||||
}
|
||||
zone_import = objects.ZoneTask(**values)
|
||||
|
||||
created_zone_import = self.storage.create_zone_task(context,
|
||||
zone_import)
|
||||
|
||||
self.tg.add_thread(self._import_zone, context, created_zone_import,
|
||||
request_body)
|
||||
|
||||
return created_zone_import
|
||||
|
||||
def _import_zone(self, context, zone_import, request_body):
|
||||
|
||||
def _import(self, context, zone_import, request_body):
|
||||
# Dnspython needs a str instead of a unicode object
|
||||
request_body = str(request_body)
|
||||
domain = None
|
||||
try:
|
||||
dnspython_zone = dnszone.from_text(
|
||||
request_body,
|
||||
# Don't relativize, or we end up with '@' record names.
|
||||
relativize=False,
|
||||
# Dont check origin, we allow missing NS records
|
||||
# (missing SOA records are taken care of in _create_zone).
|
||||
check_origin=False)
|
||||
domain = dnsutils.from_dnspython_zone(dnspython_zone)
|
||||
domain.type = 'PRIMARY'
|
||||
|
||||
for rrset in list(domain.recordsets):
|
||||
if rrset.type in ('NS', 'SOA'):
|
||||
domain.recordsets.remove(rrset)
|
||||
|
||||
except dnszone.UnknownOrigin:
|
||||
zone_import.message = ('The $ORIGIN statement is required and'
|
||||
' must be the first statement in the'
|
||||
' zonefile.')
|
||||
zone_import.status = 'ERROR'
|
||||
except dnsexception.SyntaxError:
|
||||
zone_import.message = 'Malformed zonefile.'
|
||||
zone_import.status = 'ERROR'
|
||||
except exceptions.BadRequest:
|
||||
zone_import.message = 'An SOA record is required.'
|
||||
zone_import.status = 'ERROR'
|
||||
except Exception:
|
||||
zone_import.message = 'An undefined error occured.'
|
||||
zone_import.status = 'ERROR'
|
||||
|
||||
return domain, zone_import
|
||||
|
||||
# Execute the import in a real Python thread
|
||||
domain, zone_import = tpool.execute(_import, self, context,
|
||||
zone_import, request_body)
|
||||
|
||||
# If the zone import was valid, create the domain
|
||||
if zone_import.status != 'ERROR':
|
||||
try:
|
||||
zone = self.create_domain(context, domain)
|
||||
zone_import.status = 'COMPLETE'
|
||||
zone_import.domain_id = zone.id
|
||||
zone_import.message = '%(name)s imported' % {'name':
|
||||
zone.name}
|
||||
except exceptions.DuplicateDomain:
|
||||
zone_import.status = 'ERROR'
|
||||
zone_import.message = 'Duplicate zone.'
|
||||
except exceptions.InvalidTTL as e:
|
||||
zone_import.status = 'ERROR'
|
||||
zone_import.message = e.message
|
||||
except Exception:
|
||||
zone_import.message = 'An undefined error occured.'
|
||||
zone_import.status = 'ERROR'
|
||||
|
||||
self.update_zone_import(context, zone_import)
|
||||
|
||||
def find_zone_imports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('find_zone_imports', context, target)
|
||||
|
||||
criterion = {
|
||||
'task_type': 'IMPORT'
|
||||
}
|
||||
return self.storage.find_zone_tasks(context, criterion, marker,
|
||||
limit, sort_key, sort_dir)
|
||||
|
||||
def get_zone_import(self, context, zone_import_id):
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('get_zone_import', context, target)
|
||||
return self.storage.get_zone_task(context, zone_import_id)
|
||||
|
||||
@notification('dns.zone_import.update')
|
||||
def update_zone_import(self, context, zone_import):
|
||||
target = {
|
||||
'tenant_id': zone_import.tenant_id,
|
||||
}
|
||||
policy.check('update_zone_import', context, target)
|
||||
|
||||
return self.storage.update_zone_task(context, zone_import)
|
||||
|
||||
@notification('dns.zone_import.delete')
|
||||
@transaction
|
||||
def delete_zone_import(self, context, zone_import_id):
|
||||
target = {
|
||||
'zone_import_id': zone_import_id,
|
||||
'tenant_id': context.tenant
|
||||
}
|
||||
policy.check('delete_zone_import', context, target)
|
||||
|
||||
zone_import = self.storage.delete_zone_task(context, zone_import_id)
|
||||
|
||||
return zone_import
|
||||
|
@ -259,6 +259,10 @@ class DuplicatePoolNsRecord(Duplicate):
|
||||
error_type = 'duplicate_pool_ns_record'
|
||||
|
||||
|
||||
class DuplicateZoneTask(Duplicate):
|
||||
error_type = 'duplicate_zone_task'
|
||||
|
||||
|
||||
class MethodNotAllowed(Base):
|
||||
expected = True
|
||||
error_code = 405
|
||||
@ -343,6 +347,10 @@ class ZoneTransferAcceptNotFound(NotFound):
|
||||
error_type = 'zone_transfer_accept_not_found'
|
||||
|
||||
|
||||
class ZoneTaskNotFound(NotFound):
|
||||
error_type = 'zone_task_not_found'
|
||||
|
||||
|
||||
class LastServerDeleteNotAllowed(BadRequest):
|
||||
error_type = 'last_server_delete_not_allowed'
|
||||
|
||||
|
@ -42,6 +42,7 @@ 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
|
||||
from designate.objects.zone_task import ZoneTask, ZoneTaskList # noqa
|
||||
|
||||
# Record Types
|
||||
|
||||
|
@ -28,3 +28,4 @@ from designate.objects.adapters.api_v2.quota import QuotaAPIv2Adapter, QuotaList
|
||||
from designate.objects.adapters.api_v2.zone_transfer_accept import ZoneTransferAcceptAPIv2Adapter, ZoneTransferAcceptListAPIv2Adapter # noqa
|
||||
from designate.objects.adapters.api_v2.zone_transfer_request import ZoneTransferRequestAPIv2Adapter, ZoneTransferRequestListAPIv2Adapter # noqa
|
||||
from designate.objects.adapters.api_v2.validation_error import ValidationErrorAPIv2Adapter, ValidationErrorListAPIv2Adapter # noqa
|
||||
from designate.objects.adapters.api_v2.zone_import import ZoneImportAPIv2Adapter, ZoneImportListAPIv2Adapter # noqa
|
||||
|
@ -47,7 +47,8 @@ class APIv2Adapter(base.DesignateAdapter):
|
||||
# Check if we should include metadata
|
||||
if isinstance(list_object, obj_base.PagedListObjectMixin):
|
||||
metadata = {}
|
||||
metadata['total_count'] = list_object.total_count
|
||||
if list_object.total_count is not None:
|
||||
metadata['total_count'] = list_object.total_count
|
||||
r_list['metadata'] = metadata
|
||||
|
||||
return r_list
|
||||
|
71
designate/objects/adapters/api_v2/zone_import.py
Normal file
71
designate/objects/adapters/api_v2/zone_import.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright 2015 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@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 oslo_log import log as logging
|
||||
|
||||
from designate.objects.adapters.api_v2 import base
|
||||
from designate import objects
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZoneImportAPIv2Adapter(base.APIv2Adapter):
|
||||
|
||||
ADAPTER_OBJECT = objects.ZoneTask
|
||||
|
||||
MODIFICATIONS = {
|
||||
'fields': {
|
||||
"id": {},
|
||||
"status": {},
|
||||
"message": {},
|
||||
"zone_id": {
|
||||
'rename': 'domain_id',
|
||||
},
|
||||
"project_id": {
|
||||
'rename': 'tenant_id'
|
||||
},
|
||||
"created_at": {},
|
||||
"updated_at": {},
|
||||
"version": {},
|
||||
},
|
||||
'options': {
|
||||
'links': True,
|
||||
'resource_name': 'import',
|
||||
'collection_name': 'imports',
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _render_object(cls, object, *args, **kwargs):
|
||||
obj = super(ZoneImportAPIv2Adapter, cls)._render_object(
|
||||
object, *args, **kwargs)
|
||||
|
||||
if obj['zone_id'] is not None:
|
||||
obj['links']['zone'] = \
|
||||
'%s/v2/%s/%s' % (cls.BASE_URI, 'zones', obj['zone_id'])
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class ZoneImportListAPIv2Adapter(base.APIv2Adapter):
|
||||
|
||||
ADAPTER_OBJECT = objects.ZoneTaskList
|
||||
|
||||
MODIFICATIONS = {
|
||||
'options': {
|
||||
'links': True,
|
||||
'resource_name': 'import',
|
||||
'collection_name': 'imports',
|
||||
}
|
||||
}
|
61
designate/objects/zone_task.py
Normal file
61
designate/objects/zone_task.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Copyright 2015 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@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.objects import base
|
||||
|
||||
|
||||
class ZoneTask(base.DictObjectMixin, base.PersistentObjectMixin,
|
||||
base.DesignateObject):
|
||||
FIELDS = {
|
||||
'status': {
|
||||
'schema': {
|
||||
"type": "string",
|
||||
"enum": ["ACTIVE", "PENDING", "DELETED", "ERROR", "COMPLETE"],
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'task_type': {
|
||||
'schema': {
|
||||
"type": "string",
|
||||
"enum": ["IMPORT"],
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'tenant_id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'message': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'maxLength': 160
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'domain_id': {
|
||||
'schema': {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ZoneTaskList(base.ListObjectMixin, base.DesignateObject,
|
||||
base.PagedListObjectMixin):
|
||||
LIST_ITEM_TYPE = ZoneTask
|
@ -630,6 +630,67 @@ class Storage(DriverPlugin):
|
||||
:param pool_attribute_id: The ID of the PoolAttribute to be deleted
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_zone_task(self, context, zone_task):
|
||||
"""
|
||||
Create a Zone Task.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_task: Tld object with the values to be created.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_zone_task(self, context, zone_task_id):
|
||||
"""
|
||||
Get a Zone Task via ID.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_task_id: Zone Task ID to get.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_zone_tasks(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
"""
|
||||
Find Zone Tasks
|
||||
|
||||
:param context: RPC Context.
|
||||
:param criterion: Criteria to filter by.
|
||||
:param marker: Resource ID from which after the requested page will
|
||||
start after
|
||||
:param limit: Integer limit of objects of the page size after the
|
||||
marker
|
||||
:param sort_key: Key from which to sort after.
|
||||
:param sort_dir: Direction to sort after using sort_key.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_zone_task(self, context, criterion):
|
||||
"""
|
||||
Find a single Zone Task.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param criterion: Criteria to filter by.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_zone_task(self, context, zone_task):
|
||||
"""
|
||||
Update a Zone Task
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_task: Zone Task to update.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_zone_task(self, context, zone_task_id):
|
||||
"""
|
||||
Delete a Zone Task via ID.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_task_id: Delete a Zone Task via ID
|
||||
"""
|
||||
|
||||
def ping(self, context):
|
||||
"""Ping the Storage connection"""
|
||||
return {
|
||||
|
@ -1122,6 +1122,43 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
||||
zone_transfer_accept,
|
||||
exceptions.ZoneTransferAcceptNotFound)
|
||||
|
||||
# Zone Task Methods
|
||||
def _find_zone_tasks(self, context, criterion, one=False, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
return self._find(
|
||||
context, tables.zone_tasks, objects.ZoneTask,
|
||||
objects.ZoneTaskList, exceptions.ZoneTaskNotFound, criterion,
|
||||
one, marker, limit, sort_key, sort_dir)
|
||||
|
||||
def create_zone_task(self, context, zone_task):
|
||||
return self._create(
|
||||
tables.zone_tasks, zone_task, exceptions.DuplicateZoneTask)
|
||||
|
||||
def get_zone_task(self, context, zone_task_id):
|
||||
return self._find_zone_tasks(context, {'id': zone_task_id},
|
||||
one=True)
|
||||
|
||||
def find_zone_tasks(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
return self._find_zone_tasks(context, criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
def find_zone_task(self, context, criterion):
|
||||
return self._find_zone_tasks(context, criterion, one=True)
|
||||
|
||||
def update_zone_task(self, context, zone_task):
|
||||
return self._update(
|
||||
context, tables.zone_tasks, zone_task,
|
||||
exceptions.DuplicateZoneTask, exceptions.ZoneTaskNotFound)
|
||||
|
||||
def delete_zone_task(self, context, zone_task_id):
|
||||
# Fetch the existing zone_task, we'll need to return it.
|
||||
zone_task = self._find_zone_tasks(context, {'id': zone_task_id},
|
||||
one=True)
|
||||
return self._delete(context, tables.zone_tasks, zone_task,
|
||||
exceptions.ZoneTaskNotFound)
|
||||
|
||||
# diagnostics
|
||||
def ping(self, context):
|
||||
start_time = time.time()
|
||||
|
@ -0,0 +1,60 @@
|
||||
# Copyright 2015 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@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 sqlalchemy import Integer, String, DateTime, Enum
|
||||
from sqlalchemy.schema import Table, Column, MetaData
|
||||
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from designate import utils
|
||||
from designate.sqlalchemy.types import UUID
|
||||
|
||||
meta = MetaData()
|
||||
TASK_STATUSES = ['ACTIVE', 'PENDING', 'DELETED', 'ERROR', 'COMPLETE']
|
||||
TASK_TYPES = ['IMPORT']
|
||||
|
||||
zone_tasks_table = Table('zone_tasks', meta,
|
||||
Column('id', UUID(), default=utils.generate_uuid, primary_key=True),
|
||||
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
|
||||
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
|
||||
Column('version', Integer(), default=1, nullable=False),
|
||||
Column('tenant_id', String(36), default=None, nullable=True),
|
||||
|
||||
Column('domain_id', UUID(), nullable=True),
|
||||
Column('task_type', Enum(name='task_types', *TASK_TYPES), nullable=True),
|
||||
Column('message', String(160), nullable=True),
|
||||
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
|
||||
nullable=False, server_default='ACTIVE',
|
||||
default='ACTIVE'),
|
||||
|
||||
mysql_engine='INNODB',
|
||||
mysql_charset='utf8')
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
# Create the table
|
||||
zone_tasks_table.create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
# Find the table and drop it
|
||||
zone_tasks_table = Table('zone_tasks', meta, autoload=True)
|
||||
zone_tasks_table.drop()
|
@ -39,6 +39,7 @@ ACTIONS = ['CREATE', 'DELETE', 'UPDATE', 'NONE']
|
||||
ZONE_ATTRIBUTE_KEYS = ('master',)
|
||||
|
||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
||||
ZONE_TASK_TYPES = ['IMPORT']
|
||||
|
||||
|
||||
metadata = MetaData()
|
||||
@ -307,3 +308,21 @@ zone_transfer_accepts = Table('zone_transfer_accepts', metadata,
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8',
|
||||
)
|
||||
|
||||
zone_tasks = Table('zone_tasks', metadata,
|
||||
Column('id', UUID(), default=utils.generate_uuid, primary_key=True),
|
||||
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
|
||||
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
|
||||
Column('version', Integer(), default=1, nullable=False),
|
||||
Column('tenant_id', String(36), default=None, nullable=True),
|
||||
|
||||
Column('domain_id', UUID(), nullable=True),
|
||||
Column('task_type', Enum(name='task_types', *ZONE_TASK_TYPES),
|
||||
nullable=True),
|
||||
Column('message', String(160), nullable=True),
|
||||
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
|
||||
nullable=False, server_default='ACTIVE',
|
||||
default='ACTIVE'),
|
||||
|
||||
mysql_engine='INNODB',
|
||||
mysql_charset='utf8')
|
||||
|
@ -17,6 +17,7 @@ import copy
|
||||
import functools
|
||||
import os
|
||||
import inspect
|
||||
import time
|
||||
|
||||
from testtools import testcase
|
||||
from oslotest import base
|
||||
@ -237,6 +238,23 @@ class TestCase(base.BaseTestCase):
|
||||
"target_tenant_id": "target_tenant_id"
|
||||
}]
|
||||
|
||||
zone_task_fixtures = [{
|
||||
'status': 'PENDING',
|
||||
'domain_id': None,
|
||||
'message': None,
|
||||
'task_type': 'IMPORT'
|
||||
}, {
|
||||
'status': 'ERROR',
|
||||
'domain_id': None,
|
||||
'message': None,
|
||||
'task_type': 'IMPORT'
|
||||
}, {
|
||||
'status': 'COMPLETE',
|
||||
'domain_id': '6ca6baef-3305-4ad0-a52b-a82df5752b62',
|
||||
'message': None,
|
||||
'task_type': 'IMPORT'
|
||||
}]
|
||||
|
||||
def setUp(self):
|
||||
super(TestCase, self).setUp()
|
||||
|
||||
@ -503,6 +521,13 @@ class TestCase(base.BaseTestCase):
|
||||
_values.update(values)
|
||||
return _values
|
||||
|
||||
def get_zone_task_fixture(self, fixture=0, values=None):
|
||||
values = values or {}
|
||||
|
||||
_values = copy.copy(self.zone_task_fixtures[fixture])
|
||||
_values.update(values)
|
||||
return _values
|
||||
|
||||
def create_tld(self, **kwargs):
|
||||
context = kwargs.pop('context', self.admin_context)
|
||||
fixture = kwargs.pop('fixture', 0)
|
||||
@ -646,6 +671,44 @@ class TestCase(base.BaseTestCase):
|
||||
return self.central_service.create_zone_transfer_accept(
|
||||
context, objects.ZoneTransferAccept.from_dict(values))
|
||||
|
||||
def create_zone_task(self, **kwargs):
|
||||
context = kwargs.pop('context', self.admin_context)
|
||||
fixture = kwargs.pop('fixture', 0)
|
||||
|
||||
zone_task = self.get_zone_task_fixture(fixture=fixture,
|
||||
values=kwargs)
|
||||
|
||||
return self.storage.create_zone_task(
|
||||
context, objects.ZoneTask.from_dict(zone_task))
|
||||
|
||||
def wait_for_import(self, zone_import_id, errorok=False):
|
||||
"""
|
||||
Zone imports spawn a thread to parse the zone file and
|
||||
insert the data. This waits for this process before continuing
|
||||
"""
|
||||
attempts = 0
|
||||
while attempts < 20:
|
||||
# Give the import a half second to complete
|
||||
time.sleep(.5)
|
||||
|
||||
# Retrieve it, and ensure it's the same
|
||||
zone_import = self.central_service.get_zone_import(
|
||||
self.admin_context, zone_import_id)
|
||||
|
||||
# If the import is done, we're done
|
||||
if zone_import.status == 'COMPLETE':
|
||||
break
|
||||
|
||||
# If errors are allowed, just make sure that something completed
|
||||
if errorok:
|
||||
if zone_import.status != 'PENDING':
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
|
||||
if not errorok:
|
||||
self.assertEqual(zone_import.status, 'COMPLETE')
|
||||
|
||||
def _ensure_interface(self, interface, implementation):
|
||||
for name in interface.__abstractmethods__:
|
||||
in_arginfo = inspect.getargspec(getattr(interface, name))
|
||||
|
21
designate/tests/resources/zonefiles/two_example.com.zone
Normal file
21
designate/tests/resources/zonefiles/two_example.com.zone
Normal file
@ -0,0 +1,21 @@
|
||||
$ORIGIN example2.com.
|
||||
example2.com. 600 IN SOA ns1.example2.com. nsadmin.example2.com. (
|
||||
2013091101 ; serial
|
||||
7200 ; refresh
|
||||
3600 ; retry
|
||||
2419200 ; expire
|
||||
10800 ; minimum
|
||||
)
|
||||
ipv4.example2.com. 300 IN A 192.0.0.1
|
||||
ipv6.example2.com. IN AAAA fd00::1
|
||||
cname.example2.com. IN CNAME example2.com.
|
||||
example2.com. IN MX 5 192.0.0.2
|
||||
example2.com. IN MX 10 192.0.0.3
|
||||
_http._tcp.example2.com. IN SRV 10 0 80 192.0.0.4
|
||||
_http._tcp.example2.com. IN SRV 10 5 80 192.0.0.5
|
||||
example2.com. IN TXT "abc" "def"
|
||||
example2.com. IN SPF "v=spf1 mx a"
|
||||
example2.com. IN NS ns1.example2.com.
|
||||
example2.com. IN NS ns2.example2.com.
|
||||
delegation.example2.com. IN NS ns1.example2.com.
|
||||
1.0.0.192.in-addr.arpa. IN PTR ipv4.example2.com.
|
@ -1,81 +0,0 @@
|
||||
# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from dns import zone as dnszone
|
||||
from oslo_config import cfg
|
||||
|
||||
from designate.tests.test_api.test_admin import AdminApiTestCase
|
||||
|
||||
cfg.CONF.import_opt('enabled_extensions_admin', 'designate.api.admin',
|
||||
group='service:api')
|
||||
|
||||
|
||||
class AdminApiZoneImportExportTest(AdminApiTestCase):
|
||||
def setUp(self):
|
||||
self.config(enabled_extensions_admin=['zones'], group='service:api')
|
||||
super(AdminApiZoneImportExportTest, self).setUp()
|
||||
|
||||
# Zone import/export
|
||||
def test_missing_origin(self):
|
||||
self.policy({'zone_import': '@'})
|
||||
fixture = self.get_zonefile_fixture(variant='noorigin')
|
||||
|
||||
self._assert_exception('bad_request', 400, self.client.post,
|
||||
'/zones/import',
|
||||
fixture, headers={'Content-type': 'text/dns'})
|
||||
|
||||
def test_missing_soa(self):
|
||||
self.policy({'zone_import': '@'})
|
||||
fixture = self.get_zonefile_fixture(variant='nosoa')
|
||||
|
||||
self._assert_exception('bad_request', 400, self.client.post,
|
||||
'/zones/import',
|
||||
fixture, headers={'Content-type': 'text/dns'})
|
||||
|
||||
def test_malformed_zonefile(self):
|
||||
self.policy({'zone_import': '@'})
|
||||
fixture = self.get_zonefile_fixture(variant='malformed')
|
||||
|
||||
self._assert_exception('bad_request', 400, self.client.post,
|
||||
'/zones/import',
|
||||
fixture, headers={'Content-type': 'text/dns'})
|
||||
|
||||
def test_import_export(self):
|
||||
self.policy({'default': '@'})
|
||||
# Since v2 doesn't support getting records, import and export the
|
||||
# fixture, making sure they're the same according to dnspython
|
||||
post_response = self.client.post('/zones/import',
|
||||
self.get_zonefile_fixture(),
|
||||
headers={'Content-type': 'text/dns'})
|
||||
get_response = self.client.get('/zones/export/%s' %
|
||||
post_response.json['id'],
|
||||
headers={'Accept': 'text/dns'})
|
||||
|
||||
exported_zonefile = get_response.body
|
||||
imported = dnszone.from_text(self.get_zonefile_fixture())
|
||||
exported = dnszone.from_text(exported_zonefile)
|
||||
# Compare SOA emails, since zone comparison takes care of origin
|
||||
imported_soa = imported.get_rdataset(imported.origin, 'SOA')
|
||||
imported_email = imported_soa[0].rname.to_text()
|
||||
exported_soa = exported.get_rdataset(exported.origin, 'SOA')
|
||||
exported_email = exported_soa[0].rname.to_text()
|
||||
self.assertEqual(imported_email, exported_email)
|
||||
# Delete SOAs since they have, at the very least, different serials,
|
||||
# and dnspython considers that to be not equal.
|
||||
imported.delete_rdataset(imported.origin, 'SOA')
|
||||
exported.delete_rdataset(exported.origin, 'SOA')
|
||||
# Delete NS records, since they won't be the same
|
||||
imported.delete_rdataset(imported.origin, 'NS')
|
||||
exported.delete_rdataset(exported.origin, 'NS')
|
||||
imported.delete_rdataset('delegation', 'NS')
|
||||
self.assertEqual(imported, exported)
|
131
designate/tests/test_api/test_v2/test_import_export.py
Normal file
131
designate/tests/test_api/test_v2/test_import_export.py
Normal file
@ -0,0 +1,131 @@
|
||||
# COPYRIGHT 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from dns import zone as dnszone
|
||||
from webtest import TestApp
|
||||
from oslo_config import cfg
|
||||
|
||||
from designate.api import admin as admin_api
|
||||
from designate.api import middleware
|
||||
from designate.tests.test_api.test_v2 import ApiV2TestCase
|
||||
|
||||
|
||||
cfg.CONF.import_opt('enabled_extensions_admin', 'designate.api.admin',
|
||||
group='service:api')
|
||||
|
||||
|
||||
class APIV2ZoneImportExportTest(ApiV2TestCase):
|
||||
def setUp(self):
|
||||
super(APIV2ZoneImportExportTest, self).setUp()
|
||||
|
||||
self.config(enable_api_admin=True, group='service:api')
|
||||
self.config(enabled_extensions_admin=['zones'], group='service:api')
|
||||
# Create the application
|
||||
adminapp = admin_api.factory({})
|
||||
# Inject the NormalizeURIMiddleware middleware
|
||||
adminapp = middleware.NormalizeURIMiddleware(adminapp)
|
||||
# Inject the FaultWrapper middleware
|
||||
adminapp = middleware.FaultWrapperMiddleware(adminapp)
|
||||
# Inject the TestContext middleware
|
||||
adminapp = middleware.TestContextMiddleware(
|
||||
adminapp, self.admin_context.tenant,
|
||||
self.admin_context.tenant)
|
||||
# Obtain a test client
|
||||
self.adminclient = TestApp(adminapp)
|
||||
|
||||
# # Zone import/export
|
||||
def test_missing_origin(self):
|
||||
fixture = self.get_zonefile_fixture(variant='noorigin')
|
||||
|
||||
response = self.client.post_json('/zones/tasks/imports', fixture,
|
||||
headers={'Content-type': 'text/dns'})
|
||||
|
||||
import_id = response.json_body['id']
|
||||
self.wait_for_import(import_id, errorok=True)
|
||||
|
||||
url = '/zones/tasks/imports/%s' % import_id
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.json['status'], 'ERROR')
|
||||
origin_msg = ("The $ORIGIN statement is required and must be the"
|
||||
" first statement in the zonefile.")
|
||||
self.assertEqual(response.json['message'], origin_msg)
|
||||
|
||||
def test_missing_soa(self):
|
||||
fixture = self.get_zonefile_fixture(variant='nosoa')
|
||||
|
||||
response = self.client.post_json('/zones/tasks/imports', fixture,
|
||||
headers={'Content-type': 'text/dns'})
|
||||
|
||||
import_id = response.json_body['id']
|
||||
self.wait_for_import(import_id, errorok=True)
|
||||
|
||||
url = '/zones/tasks/imports/%s' % import_id
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.json['status'], 'ERROR')
|
||||
origin_msg = ("Malformed zonefile.")
|
||||
self.assertEqual(response.json['message'], origin_msg)
|
||||
|
||||
def test_malformed_zonefile(self):
|
||||
fixture = self.get_zonefile_fixture(variant='malformed')
|
||||
|
||||
response = self.client.post_json('/zones/tasks/imports', fixture,
|
||||
headers={'Content-type': 'text/dns'})
|
||||
|
||||
import_id = response.json_body['id']
|
||||
self.wait_for_import(import_id, errorok=True)
|
||||
|
||||
url = '/zones/tasks/imports/%s' % import_id
|
||||
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.json['status'], 'ERROR')
|
||||
origin_msg = ("Malformed zonefile.")
|
||||
self.assertEqual(response.json['message'], origin_msg)
|
||||
|
||||
def test_import_export(self):
|
||||
# Since v2 doesn't support getting records, import and export the
|
||||
# fixture, making sure they're the same according to dnspython
|
||||
post_response = self.client.post('/zones/tasks/imports',
|
||||
self.get_zonefile_fixture(),
|
||||
headers={'Content-type': 'text/dns'})
|
||||
|
||||
import_id = post_response.json_body['id']
|
||||
self.wait_for_import(import_id)
|
||||
|
||||
url = '/zones/tasks/imports/%s' % import_id
|
||||
response = self.client.get(url)
|
||||
|
||||
self.policy({'zone_export': '@'})
|
||||
get_response = self.adminclient.get('/zones/export/%s' %
|
||||
response.json['zone_id'],
|
||||
headers={'Accept': 'text/dns'})
|
||||
|
||||
exported_zonefile = get_response.body
|
||||
imported = dnszone.from_text(self.get_zonefile_fixture())
|
||||
exported = dnszone.from_text(exported_zonefile)
|
||||
# Compare SOA emails, since zone comparison takes care of origin
|
||||
imported_soa = imported.get_rdataset(imported.origin, 'SOA')
|
||||
imported_email = imported_soa[0].rname.to_text()
|
||||
exported_soa = exported.get_rdataset(exported.origin, 'SOA')
|
||||
exported_email = exported_soa[0].rname.to_text()
|
||||
self.assertEqual(imported_email, exported_email)
|
||||
# Delete SOAs since they have, at the very least, different serials,
|
||||
# and dnspython considers that to be not equal.
|
||||
imported.delete_rdataset(imported.origin, 'SOA')
|
||||
exported.delete_rdataset(exported.origin, 'SOA')
|
||||
# Delete NS records, since they won't be the same
|
||||
imported.delete_rdataset(imported.origin, 'NS')
|
||||
exported.delete_rdataset(exported.origin, 'NS')
|
||||
imported.delete_rdataset('delegation', 'NS')
|
||||
self.assertEqual(imported, exported)
|
@ -2913,3 +2913,112 @@ class CentralServiceTest(CentralTestCase):
|
||||
zone_transfer_accept = \
|
||||
self.central_service.create_zone_transfer_accept(
|
||||
tenant_3_context, zone_transfer_accept)
|
||||
|
||||
# Zone Import Tests
|
||||
def test_create_zone_import(self):
|
||||
# Create a Zone Import
|
||||
context = self.get_context()
|
||||
request_body = self.get_zonefile_fixture()
|
||||
zone_import = self.central_service.create_zone_import(context,
|
||||
request_body)
|
||||
|
||||
# Ensure all values have been set correctly
|
||||
self.assertIsNotNone(zone_import['id'])
|
||||
self.assertEqual(zone_import.status, 'PENDING')
|
||||
self.assertEqual(zone_import.message, None)
|
||||
self.assertEqual(zone_import.domain_id, None)
|
||||
|
||||
self.wait_for_import(zone_import.id)
|
||||
|
||||
def test_find_zone_imports(self):
|
||||
context = self.get_context()
|
||||
|
||||
# Ensure we have no zone_imports to start with.
|
||||
zone_imports = self.central_service.find_zone_imports(
|
||||
self.admin_context)
|
||||
self.assertEqual(len(zone_imports), 0)
|
||||
|
||||
# Create a single zone_import
|
||||
request_body = self.get_zonefile_fixture()
|
||||
self.central_service.create_zone_import(context, request_body)
|
||||
|
||||
# Ensure we can retrieve the newly created zone_import
|
||||
zone_imports = self.central_service.find_zone_imports(
|
||||
self.admin_context)
|
||||
self.assertEqual(len(zone_imports), 1)
|
||||
|
||||
# Create a second zone_import
|
||||
request_body = self.get_zonefile_fixture(variant="two")
|
||||
zone_import = self.central_service.create_zone_import(context,
|
||||
request_body)
|
||||
|
||||
# Wait for the imports to complete
|
||||
self.wait_for_import(zone_import.id)
|
||||
|
||||
# Ensure we can retrieve both zone_imports
|
||||
zone_imports = self.central_service.find_zone_imports(
|
||||
self.admin_context)
|
||||
self.assertEqual(len(zone_imports), 2)
|
||||
self.assertEqual(zone_imports[0].status, 'COMPLETE')
|
||||
self.assertEqual(zone_imports[1].status, 'COMPLETE')
|
||||
|
||||
def test_get_zone_import(self):
|
||||
# Create a Zone Import
|
||||
context = self.get_context()
|
||||
request_body = self.get_zonefile_fixture()
|
||||
zone_import = self.central_service.create_zone_import(
|
||||
context, request_body)
|
||||
|
||||
# Wait for the import to complete
|
||||
# time.sleep(1)
|
||||
self.wait_for_import(zone_import.id)
|
||||
|
||||
# Retrieve it, and ensure it's the same
|
||||
zone_import = self.central_service.get_zone_import(
|
||||
self.admin_context, zone_import.id)
|
||||
|
||||
self.assertEqual(zone_import['id'], zone_import.id)
|
||||
self.assertEqual(zone_import['status'], zone_import.status)
|
||||
self.assertEqual('COMPLETE', zone_import.status)
|
||||
|
||||
def test_update_zone_import(self):
|
||||
# Create a Zone Import
|
||||
context = self.get_context()
|
||||
request_body = self.get_zonefile_fixture()
|
||||
zone_import = self.central_service.create_zone_import(
|
||||
context, request_body)
|
||||
|
||||
self.wait_for_import(zone_import.id)
|
||||
|
||||
# Update the Object
|
||||
zone_import.message = 'test message'
|
||||
|
||||
# Perform the update
|
||||
zone_import = self.central_service.update_zone_import(
|
||||
self.admin_context, zone_import)
|
||||
|
||||
# Fetch the zone_import again
|
||||
zone_import = self.central_service.get_zone_import(context,
|
||||
zone_import.id)
|
||||
|
||||
# Ensure the zone_import was updated correctly
|
||||
self.assertEqual('test message', zone_import.message)
|
||||
|
||||
def test_delete_zone_import(self):
|
||||
# Create a Zone Import
|
||||
context = self.get_context()
|
||||
request_body = self.get_zonefile_fixture()
|
||||
zone_import = self.central_service.create_zone_import(
|
||||
context, request_body)
|
||||
|
||||
self.wait_for_import(zone_import.id)
|
||||
|
||||
# Delete the zone_import
|
||||
self.central_service.delete_zone_import(context,
|
||||
zone_import['id'])
|
||||
|
||||
# Fetch the zone_import again, ensuring an exception is raised
|
||||
self.assertRaises(
|
||||
exceptions.ZoneTaskNotFound,
|
||||
self.central_service.get_zone_import,
|
||||
context, zone_import['id'])
|
||||
|
@ -2346,3 +2346,123 @@ class StorageTestCase(object):
|
||||
|
||||
with testtools.ExpectedException(exceptions.DuplicatePoolAttribute):
|
||||
self.create_pool_attribute(fixture=0)
|
||||
|
||||
# Zone Import Tests
|
||||
def test_create_zone_task(self):
|
||||
values = {
|
||||
'status': 'PENDING',
|
||||
'task_type': 'IMPORT'
|
||||
}
|
||||
|
||||
result = self.storage.create_zone_task(
|
||||
self.admin_context, objects.ZoneTask.from_dict(values))
|
||||
|
||||
self.assertIsNotNone(result['id'])
|
||||
self.assertIsNotNone(result['created_at'])
|
||||
self.assertIsNone(result['updated_at'])
|
||||
self.assertIsNotNone(result['version'])
|
||||
self.assertEqual(result['status'], values['status'])
|
||||
self.assertEqual(result['domain_id'], None)
|
||||
self.assertEqual(result['message'], None)
|
||||
|
||||
def test_find_zone_tasks(self):
|
||||
|
||||
actual = self.storage.find_zone_tasks(self.admin_context)
|
||||
self.assertEqual(0, len(actual))
|
||||
|
||||
# Create a single ZoneTask
|
||||
zone_task = self.create_zone_task(fixture=0)
|
||||
|
||||
actual = self.storage.find_zone_tasks(self.admin_context)
|
||||
self.assertEqual(1, len(actual))
|
||||
|
||||
self.assertEqual(zone_task['status'], actual[0]['status'])
|
||||
self.assertEqual(zone_task['message'], actual[0]['message'])
|
||||
self.assertEqual(zone_task['domain_id'], actual[0]['domain_id'])
|
||||
|
||||
def test_find_zone_tasks_paging(self):
|
||||
# Create 10 ZoneTasks
|
||||
created = [self.create_zone_task() for i in xrange(10)]
|
||||
|
||||
# Ensure we can page through the results.
|
||||
self._ensure_paging(created, self.storage.find_zone_tasks)
|
||||
|
||||
def test_find_zone_tasks_with_criterion(self):
|
||||
zone_task_one = self.create_zone_task(fixture=0)
|
||||
zone_task_two = self.create_zone_task(fixture=1)
|
||||
|
||||
criterion_one = dict(status=zone_task_one['status'])
|
||||
|
||||
results = self.storage.find_zone_tasks(self.admin_context,
|
||||
criterion_one)
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
self.assertEqual(results[0]['status'], zone_task_one['status'])
|
||||
|
||||
criterion_two = dict(status=zone_task_two['status'])
|
||||
|
||||
results = self.storage.find_zone_tasks(self.admin_context,
|
||||
criterion_two)
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
self.assertEqual(results[0]['status'], zone_task_two['status'])
|
||||
|
||||
def test_get_zone_task(self):
|
||||
# Create a zone_task
|
||||
expected = self.create_zone_task()
|
||||
actual = self.storage.get_zone_task(self.admin_context,
|
||||
expected['id'])
|
||||
|
||||
self.assertEqual(actual['status'], expected['status'])
|
||||
|
||||
def test_get_zone_task_missing(self):
|
||||
with testtools.ExpectedException(exceptions.ZoneTaskNotFound):
|
||||
uuid = '4c8e7f82-3519-4bf7-8940-a66a4480f223'
|
||||
self.storage.get_zone_task(self.admin_context, uuid)
|
||||
|
||||
def test_find_zone_task_criterion_missing(self):
|
||||
expected = self.create_zone_task()
|
||||
|
||||
criterion = dict(status=expected['status'] + "NOT FOUND")
|
||||
|
||||
with testtools.ExpectedException(exceptions.ZoneTaskNotFound):
|
||||
self.storage.find_zone_task(self.admin_context, criterion)
|
||||
|
||||
def test_update_zone_task(self):
|
||||
# Create a zone_task
|
||||
zone_task = self.create_zone_task(status='PENDING', task_type='IMPORT')
|
||||
|
||||
# Update the zone_task
|
||||
zone_task.status = 'COMPLETE'
|
||||
|
||||
# Update storage
|
||||
zone_task = self.storage.update_zone_task(self.admin_context,
|
||||
zone_task)
|
||||
|
||||
# Verify the new value
|
||||
self.assertEqual('COMPLETE', zone_task.status)
|
||||
|
||||
# Ensure the version column was incremented
|
||||
self.assertEqual(2, zone_task.version)
|
||||
|
||||
def test_update_zone_task_missing(self):
|
||||
zone_task = objects.ZoneTask(
|
||||
id='486f9cbe-b8b6-4d8c-8275-1a6e47b13e00')
|
||||
with testtools.ExpectedException(exceptions.ZoneTaskNotFound):
|
||||
self.storage.update_zone_task(self.admin_context, zone_task)
|
||||
|
||||
def test_delete_zone_task(self):
|
||||
# Create a zone_task
|
||||
zone_task = self.create_zone_task()
|
||||
|
||||
# Delete the zone_task
|
||||
self.storage.delete_zone_task(self.admin_context, zone_task['id'])
|
||||
|
||||
# Verify that it's deleted
|
||||
with testtools.ExpectedException(exceptions.ZoneTaskNotFound):
|
||||
self.storage.get_zone_task(self.admin_context, zone_task['id'])
|
||||
|
||||
def test_delete_zone_task_missing(self):
|
||||
with testtools.ExpectedException(exceptions.ZoneTaskNotFound):
|
||||
uuid = 'cac1fc02-79b2-4e62-a1a4-427b6790bbe6'
|
||||
self.storage.delete_zone_task(self.admin_context, uuid)
|
||||
|
@ -88,7 +88,6 @@ V2 API
|
||||
rest/v2/recordsets
|
||||
rest/v2/tlds
|
||||
rest/v2/blacklists
|
||||
rest/v2/quotas
|
||||
rest/v2/pools
|
||||
|
||||
Admin API
|
||||
@ -98,4 +97,5 @@ Admin API
|
||||
:glob:
|
||||
|
||||
rest/admin/quotas
|
||||
rest/admin/zones
|
||||
|
||||
|
@ -3,7 +3,7 @@ Zones
|
||||
|
||||
Overview
|
||||
--------
|
||||
The zones extension can be used to import and export zonesfiles to designate.
|
||||
The zones extension can be used to export zonesfiles from designate.
|
||||
|
||||
*Note*: Zones is an extension and needs to be enabled before it can be used.
|
||||
If Designate returns a 404 error, ensure that the following line has been
|
||||
@ -57,58 +57,3 @@ Export Zone
|
||||
:statuscode 406: Not Acceptable
|
||||
|
||||
Notice how the SOA and NS records are replaced with the Designate server(s).
|
||||
|
||||
Import Zone
|
||||
-----------
|
||||
|
||||
.. http:post:: /admin/zones/import
|
||||
|
||||
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).
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /admin/zones/import HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Content-type: text/dns
|
||||
|
||||
$ORIGIN example.com.
|
||||
example.com. 42 IN SOA ns.example.com. nsadmin.example.com. 42 42 42 42 42
|
||||
example.com. 42 IN NS ns.example.com.
|
||||
example.com. 42 IN MX 10 mail.example.com.
|
||||
ns.example.com. 42 IN A 10.0.0.1
|
||||
mail.example.com. 42 IN A 10.0.0.2
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "nsadmin@example.com",
|
||||
"id": "6b78734a-aef1-45cd-9708-8eb3c2d26ff1",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/6b78734a-aef1-45cd-9708-8eb3c2d26ff1"
|
||||
},
|
||||
"name": "example.com.",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "d7accc2f8ce343318386886953f2fc6a",
|
||||
"serial": 1404757531,
|
||||
"ttl": "42",
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"version": 1,
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null
|
||||
}
|
||||
|
||||
:statuscode 201: Created
|
||||
:statuscode 415: Unsupported Media Type
|
||||
:statuscode 400: Bad request
|
||||
|
@ -559,3 +559,192 @@ Accept a Transfer Request
|
||||
"status": "COMPLETE"
|
||||
}
|
||||
|
||||
|
||||
Import Zone
|
||||
-----------
|
||||
|
||||
Create a Zone Import
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:post:: /zones/tasks/imports
|
||||
|
||||
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).
|
||||
|
||||
An object will be returned that can be queried using the 'self' link the
|
||||
'links' field.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v2/zones/tasks/imports HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Content-type: text/dns
|
||||
|
||||
$ORIGIN example.com.
|
||||
example.com. 42 IN SOA ns.example.com. nsadmin.example.com. 42 42 42 42 42
|
||||
example.com. 42 IN NS ns.example.com.
|
||||
example.com. 42 IN MX 10 mail.example.com.
|
||||
ns.example.com. 42 IN A 10.0.0.1
|
||||
mail.example.com. 42 IN A 10.0.0.2
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "PENDING",
|
||||
"zone_id": null,
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-fe87-4cbb-b10b-21a06e215d41"
|
||||
},
|
||||
"created_at": "2015-05-08T15:43:42.000000",
|
||||
"updated_at": null,
|
||||
"version": 1,
|
||||
"message": null,
|
||||
"project_id": "1",
|
||||
"id": "074e805e-fe87-4cbb-b10b-21a06e215d41"
|
||||
}
|
||||
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 415: Unsupported Media Type
|
||||
|
||||
|
||||
View a Zone Import
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:get:: /zones/tasks/imports/(uuid:id)
|
||||
|
||||
The status of a zone import can be viewed by querying the id
|
||||
given when the request was created.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/tasks/imports/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 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
|
||||
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"zone_id": "6625198b-d67d-47dc-8d29-f90bd60f3ac4",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-fe87-4cbb-b10b-21a06e215d41",
|
||||
"href": "http://127.0.0.1:9001/v2/zones/6625198b-d67d-47dc-8d29-f90bd60f3ac4"
|
||||
},
|
||||
"created_at": "2015-05-08T15:43:42.000000",
|
||||
"updated_at": "2015-05-08T15:43:42.000000",
|
||||
"version": 2,
|
||||
"message": "example.com. imported",
|
||||
"project_id": "noauth-project",
|
||||
"id": "074e805e-fe87-4cbb-b10b-21a06e215d41"
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
:statuscode 404: Not Found
|
||||
|
||||
Notice the status has been updated, the message field shows that the zone was
|
||||
successfully imported, and there is now a 'href' in the 'links' field that points
|
||||
to the new zone.
|
||||
|
||||
List Zone Imports
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:get:: /zones/tasks/imports/
|
||||
|
||||
List all of the zone imports created by this project.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/tasks/imports/ 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
|
||||
|
||||
{
|
||||
"imports": [
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"zone_id": "ea2fd415-dc6d-401c-a8af-90a89d7efcf9",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/imports/fb47a23e-eb97-4c86-a3d4-f3e1a4ca9f5e",
|
||||
"href": "http://127.0.0.1:9001/v2/zones/ea2fd415-dc6d-401c-a8af-90a89d7efcf9"
|
||||
},
|
||||
"created_at": "2015-05-08T15:22:50.000000",
|
||||
"updated_at": "2015-05-08T15:22:50.000000",
|
||||
"version": 2,
|
||||
"message": "example.com. imported",
|
||||
"project_id": "noauth-project",
|
||||
"id": "fb47a23e-eb97-4c86-a3d4-f3e1a4ca9f5e"
|
||||
},
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"zone_id": "6625198b-d67d-47dc-8d29-f90bd60f3ac4",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-fe87-4cbb-b10b-21a06e215d41",
|
||||
"href": "http://127.0.0.1:9001/v2/zones/6625198b-d67d-47dc-8d29-f90bd60f3ac4"
|
||||
},
|
||||
"created_at": "2015-05-08T15:43:42.000000",
|
||||
"updated_at": "2015-05-08T15:43:42.000000",
|
||||
"version": 2,
|
||||
"message": "example.com. imported",
|
||||
"project_id": "noauth-project",
|
||||
"id": "074e805e-fe87-4cbb-b10b-21a06e215d41"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/imports"
|
||||
}
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
:statuscode 404: Not Found
|
||||
|
||||
Delete Zone Import
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:delete:: /zones/tasks/imports/(uuid:id)
|
||||
|
||||
Deletes a zone import with the specified ID. This does not affect the zone
|
||||
that was imported, it simply removes the record of the import.
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v2/zones/tasks/imports/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
**Example Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:statuscode 204: No Content
|
||||
|
@ -126,7 +126,7 @@ debug = False
|
||||
|
||||
# Enabled Admin API extensions
|
||||
# Can be one or more of : reports, quotas, counts, tenants, zones
|
||||
# zone import / export is in zones extension
|
||||
# zone export is in zones extension
|
||||
#enabled_extensions_admin =
|
||||
|
||||
# Show the pecan HTML based debug interface (v2 only)
|
||||
|
@ -19,9 +19,6 @@
|
||||
|
||||
"use_low_ttl": "rule:admin",
|
||||
|
||||
"zone_import": "rule:admin",
|
||||
"zone_export": "rule:admin",
|
||||
|
||||
"get_quotas": "rule:admin_or_owner",
|
||||
"get_quota": "rule:admin_or_owner",
|
||||
"set_quota": "rule:admin",
|
||||
@ -109,5 +106,13 @@
|
||||
"find_zone_transfer_accepts": "rule:admin",
|
||||
"find_zone_transfer_accept": "rule:admin",
|
||||
"update_zone_transfer_accept": "rule:admin",
|
||||
"delete_zone_transfer_accept": "rule:admin"
|
||||
"delete_zone_transfer_accept": "rule:admin",
|
||||
|
||||
"zone_export": "rule:admin_or_owner",
|
||||
|
||||
"create_zone_import": "rule:admin_or_owner",
|
||||
"find_zone_imports": "rule:admin_or_owner",
|
||||
"get_zone_import": "rule:admin_or_owner",
|
||||
"update_zone_import": "rule:admin_or_owner",
|
||||
"delete_zone_import": "rule:admin_or_owner"
|
||||
}
|
||||
|
62
functionaltests/api/v2/clients/zone_import_client.py
Normal file
62
functionaltests/api/v2/clients/zone_import_client.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
Copyright 2015 Rackspace
|
||||
|
||||
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 functionaltests.api.v2.models.zone_import_model import ZoneImportModel
|
||||
from functionaltests.api.v2.models.zone_import_model import ZoneImportListModel
|
||||
from functionaltests.common.client import ClientMixin
|
||||
from functionaltests.common import utils
|
||||
|
||||
|
||||
class ZoneImportClient(ClientMixin):
|
||||
|
||||
@classmethod
|
||||
def zone_imports_uri(cls):
|
||||
return "/v2/zones/tasks/imports"
|
||||
|
||||
@classmethod
|
||||
def zone_import_uri(cls, id):
|
||||
return "{0}/{1}".format(cls.zone_imports_uri(), id)
|
||||
|
||||
def list_zone_imports(self, **kwargs):
|
||||
resp, body = self.client.get(self.zone_imports_uri(), **kwargs)
|
||||
return self.deserialize(resp, body, ZoneImportListModel)
|
||||
|
||||
def get_zone_import(self, id, **kwargs):
|
||||
resp, body = self.client.get(self.zone_import_uri(id))
|
||||
return self.deserialize(resp, body, ZoneImportModel)
|
||||
|
||||
def post_zone_import(self, zonefile_data, **kwargs):
|
||||
headers = {'Content-Type': 'text/dns'}
|
||||
resp, body = self.client.post(self.zone_imports_uri(),
|
||||
body=zonefile_data, headers=headers, **kwargs)
|
||||
return self.deserialize(resp, body, ZoneImportModel)
|
||||
|
||||
def delete_zone_import(self, id, **kwargs):
|
||||
resp, body = self.client.delete(self.zone_import_uri(id), **kwargs)
|
||||
return resp, body
|
||||
|
||||
def wait_for_zone_import(self, zone_import_id):
|
||||
utils.wait_for_condition(
|
||||
lambda: self.is_zone_import_active(zone_import_id))
|
||||
|
||||
def is_zone_import_active(self, zone_import_id):
|
||||
resp, model = self.get_zone_import(zone_import_id)
|
||||
# don't have assertEqual but still want to fail fast
|
||||
assert resp.status == 200
|
||||
if model.status == 'COMPLETE':
|
||||
return True
|
||||
elif model.status == 'ERROR':
|
||||
raise Exception("Saw ERROR status")
|
||||
return False
|
27
functionaltests/api/v2/models/zone_import_model.py
Normal file
27
functionaltests/api/v2/models/zone_import_model.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Copyright 2015 Rackspace
|
||||
|
||||
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 functionaltests.common.models import BaseModel
|
||||
from functionaltests.common.models import CollectionModel
|
||||
|
||||
|
||||
class ZoneImportModel(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ZoneImportListModel(CollectionModel):
|
||||
COLLECTION_NAME = 'imports'
|
||||
MODEL_TYPE = ZoneImportModel
|
@ -16,10 +16,12 @@ limitations under the License.
|
||||
|
||||
from tempest_lib.exceptions import Conflict
|
||||
from tempest_lib.exceptions import Forbidden
|
||||
from tempest_lib.exceptions import NotFound
|
||||
|
||||
from functionaltests.common import datagen
|
||||
from functionaltests.api.v2.base import DesignateV2Test
|
||||
from functionaltests.api.v2.clients.zone_client import ZoneClient
|
||||
from functionaltests.api.v2.clients.zone_import_client import ZoneImportClient
|
||||
|
||||
|
||||
class ZoneTest(DesignateV2Test):
|
||||
@ -105,3 +107,36 @@ class ZoneOwnershipTest(DesignateV2Test):
|
||||
self._create_zone(zone, user='default')
|
||||
self.assertRaises(Forbidden,
|
||||
lambda: self._create_zone(superzone, user='alt'))
|
||||
|
||||
|
||||
class ZoneImportTest(DesignateV2Test):
|
||||
|
||||
def setUp(self):
|
||||
super(ZoneImportTest, self).setUp()
|
||||
|
||||
def test_import_domain(self):
|
||||
user = 'default'
|
||||
import_client = ZoneImportClient.as_user(user)
|
||||
zone_client = ZoneClient.as_user(user)
|
||||
|
||||
zonefile = datagen.random_zonefile_data()
|
||||
resp, model = import_client.post_zone_import(
|
||||
zonefile)
|
||||
import_id = model.id
|
||||
self.assertEqual(resp.status, 202)
|
||||
self.assertEqual(model.status, 'PENDING')
|
||||
import_client.wait_for_zone_import(import_id)
|
||||
|
||||
resp, model = import_client.get_zone_import(
|
||||
model.id)
|
||||
self.assertEqual(resp.status, 200)
|
||||
self.assertEqual(model.status, 'COMPLETE')
|
||||
|
||||
# Wait for the zone to become 'ACTIVE'
|
||||
zone_client.wait_for_zone(model.zone_id)
|
||||
resp, zone_model = zone_client.get_zone(model.zone_id)
|
||||
|
||||
# Now make sure we can delete the zone_import
|
||||
import_client.delete_zone_import(import_id)
|
||||
self.assertRaises(NotFound,
|
||||
lambda: import_client.get_zone_import(model.id))
|
||||
|
@ -120,3 +120,18 @@ def random_pool_data():
|
||||
data["ns_records"] = []
|
||||
|
||||
return PoolModel.from_dict(data)
|
||||
|
||||
|
||||
def random_zonefile_data(name=None, ttl=None):
|
||||
"""Generate random zone data, with optional overrides
|
||||
|
||||
:return: A ZoneModel
|
||||
"""
|
||||
zone_base = ('$ORIGIN &\n& # IN SOA ns.& nsadmin.& # # # # #\n'
|
||||
'& # IN NS ns.&\n& # IN MX 10 mail.&\nns.& 360 IN A 1.0.0.1')
|
||||
if name is None:
|
||||
name = random_string(prefix='testdomain', suffix='.com.')
|
||||
if ttl is None:
|
||||
ttl = str(random.randint(1200, 8400))
|
||||
|
||||
return zone_base.replace('&', name).replace('#', ttl)
|
||||
|
Loading…
Reference in New Issue
Block a user