Asynchronous Zone Export
Do the needful to move Zone Exports to an asynchronous resource in the v2 API, as discussed at the Austin 2015 summe mid-cycle * Make designate-zone-manager an RPC service, with a read-only connection to the database * Add a 'location' column to the zone_tasks table that stores a location (swift, URI) that is used to determine where the export will be made available to the user * Add all the infrastucture to make zone export resources live (objects, central, storage methods) * Add a quota on the size of allowed synchronous exports * Tests, docs THIS DOES NOT IMPLEMENT * Zone exports to Swift * Debateable: See the note in zone_manager/service.py about how the configuration and determination of future swift exports will work. ApiImpact Blueprint: async-export Change-Id: I1c168b10358164c3ca5be986b4d615df71062851
This commit is contained in:
parent
523395dec0
commit
50d1b1553e
@ -47,6 +47,8 @@ cfg.CONF.register_opts([
|
||||
cfg.StrOpt('mdns-topic', default='mdns', help='mDNS Topic'),
|
||||
cfg.StrOpt('pool-manager-topic', default='pool_manager',
|
||||
help='Pool Manager Topic'),
|
||||
cfg.StrOpt('zone-manager-topic', default='zone_manager',
|
||||
help='Zone Manager Topic'),
|
||||
|
||||
# Default TTL
|
||||
cfg.IntOpt('default-ttl', default=3600),
|
||||
|
@ -33,6 +33,7 @@ from oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
from designate.central import rpcapi as central_rpcapi
|
||||
from designate.zone_manager import rpcapi as zone_manager_rpcapi
|
||||
from designate.i18n import _
|
||||
|
||||
|
||||
@ -54,6 +55,10 @@ class RestController(pecan.rest.RestController):
|
||||
def central_api(self):
|
||||
return central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
@property
|
||||
def zone_manager_api(self):
|
||||
return zone_manager_rpcapi.ZoneManagerAPI.get_instance()
|
||||
|
||||
def _apply_filter_params(self, params, accepted_filters, criterion):
|
||||
invalid=[]
|
||||
for k in params:
|
||||
|
@ -24,6 +24,10 @@ 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
|
||||
from designate.api.v2.controllers.zones.tasks.exports \
|
||||
import ZoneExportsController
|
||||
from designate.api.v2.controllers.zones.tasks.exports \
|
||||
import ZoneExportCreateController
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -36,3 +40,5 @@ class TasksController(object):
|
||||
abandon = abandon.AbandonController()
|
||||
xfr = XfrController()
|
||||
imports = ZoneImportController()
|
||||
exports = ZoneExportsController()
|
||||
export = ZoneExportCreateController()
|
||||
|
121
designate/api/v2/controllers/zones/tasks/exports.py
Normal file
121
designate/api/v2/controllers/zones/tasks/exports.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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 policy
|
||||
from designate import utils
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.objects.adapters.api_v2.zone_export \
|
||||
import ZoneExportAPIv2Adapter
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZoneExportController(rest.RestController):
|
||||
|
||||
@pecan.expose(template=None, content_type='text/dns')
|
||||
@utils.validate_uuid('export_id')
|
||||
def get_all(self, export_id):
|
||||
context = pecan.request.environ['context']
|
||||
policy.check('zone_export', context)
|
||||
|
||||
export = self.central_api.get_zone_export(context, export_id)
|
||||
|
||||
if export.location and export.location.startswith('designate://'):
|
||||
return self.zone_manager_api.\
|
||||
render_zone(context, export['domain_id'])
|
||||
else:
|
||||
msg = 'Zone can not be exported synchronously'
|
||||
raise exceptions.BadRequest(msg)
|
||||
|
||||
|
||||
class ZoneExportCreateController(rest.RestController):
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def post_all(self, zone_id):
|
||||
"""Create Zone Export"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
# Create the zone_export
|
||||
zone_export = self.central_api.create_zone_export(
|
||||
context, zone_id)
|
||||
response.status_int = 202
|
||||
|
||||
zone_export = ZoneExportAPIv2Adapter.render(
|
||||
'API_v2', zone_export, request=request)
|
||||
|
||||
response.headers['Location'] = zone_export['links']['self']
|
||||
return zone_export
|
||||
|
||||
|
||||
class ZoneExportsController(rest.RestController):
|
||||
|
||||
SORT_KEYS = ['created_at', 'id', 'updated_at']
|
||||
|
||||
export = ZoneExportController()
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('export_id')
|
||||
def get_one(self, export_id):
|
||||
"""Get Zone Exports"""
|
||||
|
||||
request = pecan.request
|
||||
context = request.environ['context']
|
||||
|
||||
return ZoneExportAPIv2Adapter.render(
|
||||
'API_v2',
|
||||
self.central_api.get_zone_export(
|
||||
context, export_id),
|
||||
request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def get_all(self, **params):
|
||||
"""List Zone Exports"""
|
||||
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 ZoneExportAPIv2Adapter.render(
|
||||
'API_v2',
|
||||
self.central_api.find_zone_exports(
|
||||
context, criterion, marker, limit, sort_key, sort_dir),
|
||||
request=request)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_export_id')
|
||||
def delete_one(self, zone_export_id):
|
||||
"""Delete Zone Export"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
self.central_api.delete_zone_export(context, zone_export_id)
|
||||
response.status_int = 204
|
||||
|
||||
return ''
|
@ -50,14 +50,15 @@ class CentralAPI(object):
|
||||
5.1 - Add xfr_domain
|
||||
5.2 - Add Zone Import methods
|
||||
5.3 - Add Zone Export method
|
||||
5.4 - Add asynchronous Zone Export methods
|
||||
"""
|
||||
RPC_API_VERSION = '5.3'
|
||||
RPC_API_VERSION = '5.4'
|
||||
|
||||
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.3')
|
||||
self.client = rpc.get_client(target, version_cap='5.4')
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
@ -535,3 +536,36 @@ class CentralAPI(object):
|
||||
"delete_zone_import."))
|
||||
return self.client.call(context, 'delete_zone_import',
|
||||
zone_import_id=zone_import_id)
|
||||
|
||||
# Zone Export Methods
|
||||
def create_zone_export(self, context, zone_id):
|
||||
LOG.info(_LI("create_zone_export: Calling central's "
|
||||
"create_zone_export."))
|
||||
return self.client.call(context, 'create_zone_export',
|
||||
zone_id=zone_id)
|
||||
|
||||
def find_zone_exports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
LOG.info(_LI("find_zone_exports: Calling central's "
|
||||
"find_zone_exports."))
|
||||
return self.client.call(context, 'find_zone_exports',
|
||||
criterion=criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
def get_zone_export(self, context, zone_export_id):
|
||||
LOG.info(_LI("get_zone_export: Calling central's get_zone_export."))
|
||||
return self.client.call(context, 'get_zone_export',
|
||||
zone_export_id=zone_export_id)
|
||||
|
||||
def update_zone_export(self, context, zone_export):
|
||||
LOG.info(_LI("update_zone_export: Calling central's "
|
||||
"update_zone_export."))
|
||||
return self.client.call(context, 'update_zone_export',
|
||||
zone_export=zone_export)
|
||||
|
||||
def delete_zone_export(self, context, zone_export_id):
|
||||
LOG.info(_LI("delete_zone_export: Calling central's "
|
||||
"delete_zone_export."))
|
||||
return self.client.call(context, 'delete_zone_export',
|
||||
zone_export_id=zone_export_id)
|
||||
|
@ -50,6 +50,7 @@ from designate import utils
|
||||
from designate import storage
|
||||
from designate.mdns import rpcapi as mdns_rpcapi
|
||||
from designate.pool_manager import rpcapi as pool_manager_rpcapi
|
||||
from designate.zone_manager import rpcapi as zone_manager_rpcapi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -259,7 +260,7 @@ def notification(notification_type):
|
||||
|
||||
|
||||
class Service(service.RPCService, service.Service):
|
||||
RPC_API_VERSION = '5.3'
|
||||
RPC_API_VERSION = '5.4'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -307,6 +308,10 @@ class Service(service.RPCService, service.Service):
|
||||
def pool_manager_api(self):
|
||||
return pool_manager_rpcapi.PoolManagerAPI.get_instance()
|
||||
|
||||
@property
|
||||
def zone_manager_api(self):
|
||||
return zone_manager_rpcapi.ZoneManagerAPI.get_instance()
|
||||
|
||||
def _is_valid_domain_name(self, context, domain_name):
|
||||
# Validate domain name length
|
||||
if len(domain_name) > cfg.CONF['service:central'].max_domain_name_len:
|
||||
@ -2611,3 +2616,68 @@ class Service(service.RPCService, service.Service):
|
||||
zone_import = self.storage.delete_zone_import(context, zone_import_id)
|
||||
|
||||
return zone_import
|
||||
|
||||
# Zone Export Methods
|
||||
@notification('dns.zone_export.create')
|
||||
def create_zone_export(self, context, zone_id):
|
||||
# Try getting the domain to ensure it exists
|
||||
domain = self.storage.get_domain(context, zone_id)
|
||||
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('create_zone_export', context, target)
|
||||
|
||||
values = {
|
||||
'status': 'PENDING',
|
||||
'message': None,
|
||||
'domain_id': zone_id,
|
||||
'tenant_id': context.tenant,
|
||||
'task_type': 'EXPORT'
|
||||
}
|
||||
zone_export = objects.ZoneExport(**values)
|
||||
|
||||
created_zone_export = self.storage.create_zone_export(context,
|
||||
zone_export)
|
||||
|
||||
self.zone_manager_api.start_zone_export(context, domain,
|
||||
created_zone_export)
|
||||
|
||||
return created_zone_export
|
||||
|
||||
def find_zone_exports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('find_zone_exports', context, target)
|
||||
|
||||
criterion = {
|
||||
'task_type': 'EXPORT'
|
||||
}
|
||||
return self.storage.find_zone_exports(context, criterion, marker,
|
||||
limit, sort_key, sort_dir)
|
||||
|
||||
def get_zone_export(self, context, zone_export_id):
|
||||
target = {'tenant_id': context.tenant}
|
||||
policy.check('get_zone_export', context, target)
|
||||
|
||||
return self.storage.get_zone_export(context, zone_export_id)
|
||||
|
||||
@notification('dns.zone_export.update')
|
||||
def update_zone_export(self, context, zone_export):
|
||||
target = {
|
||||
'tenant_id': zone_export.tenant_id,
|
||||
}
|
||||
policy.check('update_zone_export', context, target)
|
||||
|
||||
return self.storage.update_zone_export(context, zone_export)
|
||||
|
||||
@notification('dns.zone_export.delete')
|
||||
@transaction
|
||||
def delete_zone_export(self, context, zone_export_id):
|
||||
target = {
|
||||
'zone_export_id': zone_export_id,
|
||||
'tenant_id': context.tenant
|
||||
}
|
||||
policy.check('delete_zone_export', context, target)
|
||||
|
||||
zone_export = self.storage.delete_zone_export(context, zone_export_id)
|
||||
|
||||
return zone_export
|
||||
|
@ -290,6 +290,10 @@ class DuplicateZoneImport(Duplicate):
|
||||
error_type = 'duplicate_zone_import'
|
||||
|
||||
|
||||
class DuplicateZoneExport(Duplicate):
|
||||
error_type = 'duplicate_zone_export'
|
||||
|
||||
|
||||
class MethodNotAllowed(Base):
|
||||
expected = True
|
||||
error_code = 405
|
||||
@ -382,6 +386,10 @@ class ZoneImportNotFound(NotFound):
|
||||
error_type = 'zone_import_not_found'
|
||||
|
||||
|
||||
class ZoneExportNotFound(NotFound):
|
||||
error_type = 'zone_export_not_found'
|
||||
|
||||
|
||||
class LastServerDeleteNotAllowed(BadRequest):
|
||||
error_type = 'last_server_delete_not_allowed'
|
||||
|
||||
|
@ -44,6 +44,7 @@ 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_import import ZoneImport, ZoneImportList # noqa
|
||||
from designate.objects.zone_export import ZoneExport, ZoneExportList # noqa
|
||||
|
||||
# Record Types
|
||||
|
||||
|
@ -30,3 +30,4 @@ from designate.objects.adapters.api_v2.zone_transfer_accept import ZoneTransferA
|
||||
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
|
||||
from designate.objects.adapters.api_v2.zone_export import ZoneExportAPIv2Adapter, ZoneExportListAPIv2Adapter # noqa
|
||||
|
81
designate/objects/adapters/api_v2/zone_export.py
Normal file
81
designate/objects/adapters/api_v2/zone_export.py
Normal file
@ -0,0 +1,81 @@
|
||||
# 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 ZoneExportAPIv2Adapter(base.APIv2Adapter):
|
||||
|
||||
ADAPTER_OBJECT = objects.ZoneExport
|
||||
|
||||
MODIFICATIONS = {
|
||||
'fields': {
|
||||
"id": {},
|
||||
"status": {},
|
||||
"message": {},
|
||||
"location": {},
|
||||
"zone_id": {
|
||||
'rename': 'domain_id',
|
||||
},
|
||||
"project_id": {
|
||||
'rename': 'tenant_id'
|
||||
},
|
||||
"created_at": {},
|
||||
"updated_at": {},
|
||||
"version": {},
|
||||
},
|
||||
'options': {
|
||||
'links': True,
|
||||
'resource_name': 'export',
|
||||
'collection_name': 'exports',
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_path(cls, request):
|
||||
return '/v2/zones/tasks/exports'
|
||||
|
||||
@classmethod
|
||||
def _render_object(cls, object, *args, **kwargs):
|
||||
obj = super(ZoneExportAPIv2Adapter, cls)._render_object(
|
||||
object, *args, **kwargs)
|
||||
|
||||
if obj['location'] and obj['location'].startswith('designate://'):
|
||||
# Get the base uri from the self link, which respects host headers
|
||||
base_uri = obj['links']['self']. \
|
||||
split(cls._get_path(kwargs['request']))[0]
|
||||
|
||||
obj['links']['export'] = \
|
||||
'%s/%s' % \
|
||||
(base_uri, obj['location'].split('://')[1])
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class ZoneExportListAPIv2Adapter(base.APIv2Adapter):
|
||||
|
||||
ADAPTER_OBJECT = objects.ZoneExportList
|
||||
|
||||
MODIFICATIONS = {
|
||||
'options': {
|
||||
'links': True,
|
||||
'resource_name': 'export',
|
||||
'collection_name': 'exports',
|
||||
}
|
||||
}
|
68
designate/objects/zone_export.py
Normal file
68
designate/objects/zone_export.py
Normal file
@ -0,0 +1,68 @@
|
||||
# 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 ZoneExport(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": ["EXPORT"],
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'tenant_id': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'location': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'maxLength': 160
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'message': {
|
||||
'schema': {
|
||||
'type': ['string', 'null'],
|
||||
'maxLength': 160
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
'domain_id': {
|
||||
'schema': {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
'read_only': True
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ZoneExportList(base.ListObjectMixin, base.DesignateObject,
|
||||
base.PagedListObjectMixin):
|
||||
LIST_ITEM_TYPE = ZoneExport
|
@ -32,6 +32,8 @@ cfg.CONF.register_opts([
|
||||
help='Number of records allowed per domain'),
|
||||
cfg.IntOpt('quota-recordset-records', default=20,
|
||||
help='Number of records allowed per recordset'),
|
||||
cfg.IntOpt('quota-api-export-size', default=1000,
|
||||
help='Number of recordsets allowed in a zone export'),
|
||||
])
|
||||
|
||||
|
||||
|
@ -55,6 +55,7 @@ class Quota(DriverPlugin):
|
||||
'domain_recordsets': cfg.CONF.quota_domain_recordsets,
|
||||
'domain_records': cfg.CONF.quota_domain_records,
|
||||
'recordset_records': cfg.CONF.quota_recordset_records,
|
||||
'api_export_size': cfg.CONF.quota_api_export_size,
|
||||
}
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
|
@ -691,6 +691,67 @@ class Storage(DriverPlugin):
|
||||
:param zone_import_id: Delete a Zone Import via ID
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_zone_export(self, context, zone_export):
|
||||
"""
|
||||
Create a Zone Export.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_export: Zone Export object with the values to be created.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_zone_export(self, context, zone_export_id):
|
||||
"""
|
||||
Get a Zone Export via ID.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_export_id: Zone Export ID to get.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_zone_exports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
"""
|
||||
Find Zone Exports
|
||||
|
||||
: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_export(self, context, criterion):
|
||||
"""
|
||||
Find a single Zone Export.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param criterion: Criteria to filter by.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_zone_export(self, context, zone_export):
|
||||
"""
|
||||
Update a Zone Export
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_export: Zone Export to update.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_zone_export(self, context, zone_export_id):
|
||||
"""
|
||||
Delete a Zone Export via ID.
|
||||
|
||||
:param context: RPC Context.
|
||||
:param zone_export_id: Delete a Zone Export via ID
|
||||
"""
|
||||
|
||||
def ping(self, context):
|
||||
"""Ping the Storage connection"""
|
||||
return {
|
||||
|
@ -1299,6 +1299,46 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
||||
return self._delete(context, tables.zone_tasks, zone_import,
|
||||
exceptions.ZoneImportNotFound)
|
||||
|
||||
# Zone Export Methods
|
||||
def _find_zone_exports(self, context, criterion, one=False, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
if not criterion:
|
||||
criterion = {}
|
||||
criterion['task_type'] = 'EXPORT'
|
||||
return self._find(
|
||||
context, tables.zone_tasks, objects.ZoneExport,
|
||||
objects.ZoneExportList, exceptions.ZoneExportNotFound, criterion,
|
||||
one, marker, limit, sort_key, sort_dir)
|
||||
|
||||
def create_zone_export(self, context, zone_export):
|
||||
return self._create(
|
||||
tables.zone_tasks, zone_export, exceptions.DuplicateZoneExport)
|
||||
|
||||
def get_zone_export(self, context, zone_export_id):
|
||||
return self._find_zone_exports(context, {'id': zone_export_id},
|
||||
one=True)
|
||||
|
||||
def find_zone_exports(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
return self._find_zone_exports(context, criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
def find_zone_export(self, context, criterion):
|
||||
return self._find_zone_exports(context, criterion, one=True)
|
||||
|
||||
def update_zone_export(self, context, zone_export):
|
||||
return self._update(
|
||||
context, tables.zone_tasks, zone_export,
|
||||
exceptions.DuplicateZoneExport, exceptions.ZoneExportNotFound)
|
||||
|
||||
def delete_zone_export(self, context, zone_export_id):
|
||||
# Fetch the existing zone_export, we'll need to return it.
|
||||
zone_export = self._find_zone_exports(context, {'id': zone_export_id},
|
||||
one=True)
|
||||
return self._delete(context, tables.zone_tasks, zone_export,
|
||||
exceptions.ZoneExportNotFound)
|
||||
|
||||
# diagnostics
|
||||
def ping(self, context):
|
||||
start_time = time.time()
|
||||
|
@ -0,0 +1,44 @@
|
||||
# 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 Enum, String
|
||||
from sqlalchemy.schema import Column, MetaData, Table
|
||||
|
||||
meta = MetaData()
|
||||
TASK_TYPES = ['IMPORT', 'EXPORT']
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
dialect = migrate_engine.url.get_dialect().name
|
||||
|
||||
zone_tasks_table = Table('zone_tasks', meta, autoload=True)
|
||||
|
||||
dialect = migrate_engine.url.get_dialect().name
|
||||
|
||||
if dialect.startswith("postgresql"):
|
||||
with migrate_engine.connect() as conn:
|
||||
conn.execution_options(isolation_level="AUTOCOMMIT")
|
||||
conn.execute(
|
||||
"ALTER TYPE task_types ADD VALUE 'EXPORT' "
|
||||
"AFTER 'IMPORT'")
|
||||
conn.close()
|
||||
|
||||
zone_tasks_table.c.task_type.alter(type=Enum(name='task_type',
|
||||
*TASK_TYPES))
|
||||
|
||||
location = Column('location', String(160), nullable=True)
|
||||
location.create(zone_tasks_table)
|
@ -39,7 +39,7 @@ ACTIONS = ['CREATE', 'DELETE', 'UPDATE', 'NONE']
|
||||
ZONE_ATTRIBUTE_KEYS = ('master',)
|
||||
|
||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
||||
ZONE_TASK_TYPES = ['IMPORT']
|
||||
ZONE_TASK_TYPES = ['IMPORT', 'EXPORT']
|
||||
|
||||
|
||||
metadata = MetaData()
|
||||
@ -334,6 +334,7 @@ zone_tasks = Table('zone_tasks', metadata,
|
||||
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
|
||||
nullable=False, server_default='ACTIVE',
|
||||
default='ACTIVE'),
|
||||
Column('location', String(160), nullable=True),
|
||||
|
||||
mysql_engine='INNODB',
|
||||
mysql_charset='utf8')
|
||||
|
@ -44,6 +44,7 @@ class QuotaTestCase(tests.TestCase):
|
||||
|
||||
self.assertIsNotNone(quotas)
|
||||
self.assertEqual({
|
||||
'api_export_size': cfg.CONF.quota_api_export_size,
|
||||
'domains': cfg.CONF.quota_domains,
|
||||
'domain_recordsets': cfg.CONF.quota_domain_recordsets,
|
||||
'domain_records': cfg.CONF.quota_domain_records,
|
||||
|
@ -1753,3 +1753,112 @@ class IsSubdomainTestCase(CentralBasic):
|
||||
r = self.service._is_subdomain(self.context, 'foo.a.b.example.com.',
|
||||
'1')
|
||||
self.assertEqual(r, 'example.com.')
|
||||
|
||||
|
||||
class CentralZoneExportTests(CentralBasic):
|
||||
def setUp(self):
|
||||
super(CentralZoneExportTests, self).setUp()
|
||||
|
||||
def storage_find_tld(c, d):
|
||||
if d['name'] not in ('org',):
|
||||
raise exceptions.TldNotFound
|
||||
|
||||
self.service.storage.find_tld = storage_find_tld
|
||||
|
||||
def test_create_zone_export(self):
|
||||
self.context = Mock()
|
||||
self.context.tenant = 't'
|
||||
|
||||
self.service.storage.get_domain.return_value = RoObject(
|
||||
name='example.com.',
|
||||
id='123'
|
||||
)
|
||||
|
||||
self.service.storage.create_zone_export = Mock(
|
||||
return_value=RoObject(
|
||||
domain_id='123',
|
||||
task_type='EXPORT',
|
||||
status='PENDING',
|
||||
message=None,
|
||||
tenant_id='t'
|
||||
)
|
||||
)
|
||||
|
||||
self.service.zone_manager_api.start_zone_export = Mock()
|
||||
|
||||
out = self.service.create_zone_export(
|
||||
self.context,
|
||||
'123'
|
||||
)
|
||||
self.assertEqual(out.domain_id, '123')
|
||||
self.assertEqual(out.status, 'PENDING')
|
||||
self.assertEqual(out.task_type, 'EXPORT')
|
||||
self.assertEqual(out.message, None)
|
||||
self.assertEqual(out.tenant_id, 't')
|
||||
|
||||
def test_get_zone_export(self):
|
||||
self.context = Mock()
|
||||
self.context.tenant = 't'
|
||||
|
||||
self.service.storage.get_zone_export.return_value = RoObject(
|
||||
domain_id='123',
|
||||
task_type='EXPORT',
|
||||
status='PENDING',
|
||||
message=None,
|
||||
tenant_id='t'
|
||||
)
|
||||
|
||||
out = self.service.get_zone_export(self.context, '1')
|
||||
|
||||
n, ctx, target = designate.central.service.policy.check.call_args[0]
|
||||
|
||||
# Check arguments to policy
|
||||
self.assertEqual(target['tenant_id'], 't')
|
||||
|
||||
# Check output
|
||||
self.assertEqual(out.domain_id, '123')
|
||||
self.assertEqual(out.status, 'PENDING')
|
||||
self.assertEqual(out.task_type, 'EXPORT')
|
||||
self.assertEqual(out.message, None)
|
||||
self.assertEqual(out.tenant_id, 't')
|
||||
|
||||
def test_find_zone_exports(self):
|
||||
self.context = Mock()
|
||||
self.context.tenant = 't'
|
||||
self.service.storage.find_zone_exports = Mock()
|
||||
|
||||
self.service.find_zone_exports(self.context)
|
||||
|
||||
assert self.service.storage.find_zone_exports.called
|
||||
pcheck, ctx, target = \
|
||||
designate.central.service.policy.check.call_args[0]
|
||||
self.assertEqual(pcheck, 'find_zone_exports')
|
||||
|
||||
def test_delete_zone_export(self):
|
||||
self.context = Mock()
|
||||
self.context.tenant = 't'
|
||||
|
||||
self.service.storage.delete_zone_export = Mock(
|
||||
return_value=RoObject(
|
||||
domain_id='123',
|
||||
task_type='EXPORT',
|
||||
status='PENDING',
|
||||
message=None,
|
||||
tenant_id='t'
|
||||
)
|
||||
)
|
||||
|
||||
out = self.service.delete_zone_export(self.context, '1')
|
||||
|
||||
assert self.service.storage.delete_zone_export.called
|
||||
|
||||
self.assertEqual(out.domain_id, '123')
|
||||
self.assertEqual(out.status, 'PENDING')
|
||||
self.assertEqual(out.task_type, 'EXPORT')
|
||||
self.assertEqual(out.message, None)
|
||||
self.assertEqual(out.tenant_id, 't')
|
||||
|
||||
assert designate.central.service.policy.check.called
|
||||
pcheck, ctx, target = \
|
||||
designate.central.service.policy.check.call_args[0]
|
||||
self.assertEqual(pcheck, 'delete_zone_export')
|
||||
|
@ -27,7 +27,11 @@ OPTS = [
|
||||
cfg.IntOpt('threads', default=1000,
|
||||
help='Number of Zone Manager greenthreads to spawn'),
|
||||
cfg.ListOpt('enabled_tasks', default=None,
|
||||
help='Enabled tasks to run')
|
||||
help='Enabled tasks to run'),
|
||||
cfg.StrOpt('storage-driver', default='sqlalchemy',
|
||||
help='The storage driver to use'),
|
||||
cfg.BoolOpt('export-synchronous', default=True,
|
||||
help='Whether to allow synchronous zone exports'),
|
||||
]
|
||||
|
||||
CONF.register_opts(OPTS, group='service:zone_manager')
|
||||
|
72
designate/zone_manager/rpcapi.py
Normal file
72
designate/zone_manager/rpcapi.py
Normal file
@ -0,0 +1,72 @@
|
||||
# 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_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from designate.i18n import _LI
|
||||
from designate import rpc
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ZONE_MANAGER_API = None
|
||||
|
||||
|
||||
class ZoneManagerAPI(object):
|
||||
"""
|
||||
Client side of the zone manager RPC API.
|
||||
|
||||
API version history:
|
||||
|
||||
1.0 - Initial version
|
||||
"""
|
||||
RPC_API_VERSION = '1.0'
|
||||
|
||||
def __init__(self, topic=None):
|
||||
topic = topic if topic else cfg.CONF.zone_manager_topic
|
||||
|
||||
target = messaging.Target(topic=topic, version=self.RPC_API_VERSION)
|
||||
self.client = rpc.get_client(target, version_cap='1.0')
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""
|
||||
The rpc.get_client() which is called upon the API object initialization
|
||||
will cause a assertion error if the designate.rpc.TRANSPORT isn't setup
|
||||
by rpc.init() before.
|
||||
|
||||
This fixes that by creating the rpcapi when demanded.
|
||||
"""
|
||||
global ZONE_MANAGER_API
|
||||
if not ZONE_MANAGER_API:
|
||||
ZONE_MANAGER_API = cls()
|
||||
return ZONE_MANAGER_API
|
||||
|
||||
# Zone Export
|
||||
def start_zone_export(self, context, domain, export):
|
||||
LOG.info(_LI("start_zone_export: "
|
||||
"Calling zone_manager's start_zone_export."))
|
||||
|
||||
return self.client.cast(context, 'start_zone_export', domain=domain,
|
||||
export=export)
|
||||
|
||||
def render_zone(self, context, zone_id):
|
||||
LOG.info(_LI("render_zone: "
|
||||
"Calling zone_manager's render_zone."))
|
||||
|
||||
return self.client.call(context, 'render_zone',
|
||||
zone_id=zone_id)
|
@ -15,10 +15,16 @@
|
||||
# under the License.
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from designate.i18n import _LI
|
||||
from designate import coordination
|
||||
from designate import exceptions
|
||||
from designate import quota
|
||||
from designate import service
|
||||
from designate import storage
|
||||
from designate import utils
|
||||
from designate.central import rpcapi
|
||||
from designate.zone_manager import tasks
|
||||
|
||||
|
||||
@ -28,11 +34,29 @@ CONF = cfg.CONF
|
||||
NS = 'designate.periodic_tasks'
|
||||
|
||||
|
||||
class Service(coordination.CoordinationMixin, service.Service):
|
||||
class Service(service.RPCService, coordination.CoordinationMixin,
|
||||
service.Service):
|
||||
RPC_API_VERSION = '1.0'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
def __init__(self, threads=None):
|
||||
super(Service, self).__init__(threads=threads)
|
||||
|
||||
storage_driver = cfg.CONF['service:zone_manager'].storage_driver
|
||||
self.storage = storage.get_storage(storage_driver)
|
||||
|
||||
# Get a quota manager instance
|
||||
self.quota = quota.get_quota()
|
||||
|
||||
@property
|
||||
def service_name(self):
|
||||
return 'zone_manager'
|
||||
|
||||
@property
|
||||
def central_api(self):
|
||||
return rpcapi.CentralAPI.get_instance()
|
||||
|
||||
def start(self):
|
||||
super(Service, self).start()
|
||||
|
||||
@ -59,3 +83,67 @@ class Service(coordination.CoordinationMixin, service.Service):
|
||||
def _rebalance(self, my_partitions, members, event):
|
||||
LOG.info(_LI("Received rebalance event %s") % event)
|
||||
self.partition_range = my_partitions
|
||||
|
||||
# Begin RPC Implementation
|
||||
|
||||
# Zone Export
|
||||
def start_zone_export(self, context, domain, export):
|
||||
criterion = {'domain_id': domain.id}
|
||||
count = self.storage.count_recordsets(context, criterion)
|
||||
|
||||
export = self._determine_export_method(context, export, count)
|
||||
|
||||
self.central_api.update_zone_export(context, export)
|
||||
|
||||
def render_zone(self, context, zone_id):
|
||||
return self._export_zone(context, zone_id)
|
||||
|
||||
def _determine_export_method(self, context, export, size):
|
||||
synchronous = CONF['service:zone_manager'].export_synchronous
|
||||
|
||||
# NOTE(timsim):
|
||||
# The logic here with swift will work like this:
|
||||
# cfg.CONF.export_swift_enabled:
|
||||
# An export will land in their swift container, even if it's
|
||||
# small, but the link that comes back will be the synchronous
|
||||
# link (unless export_syncronous is False, in which case it
|
||||
# will behave like the next option)
|
||||
# cfg.CONF.export_swift_preffered:
|
||||
# The link that the user gets back will always be the swift
|
||||
# container, and status of the export resource will depend
|
||||
# on the Swift process.
|
||||
# If the export is too large for synchronous, or synchronous is not
|
||||
# enabled and swift is not enabled, it will fall through to ERROR
|
||||
# swift = False
|
||||
|
||||
if synchronous:
|
||||
try:
|
||||
self.quota.limit_check(
|
||||
context, context.tenant, api_export_size=size)
|
||||
except exceptions.OverQuota():
|
||||
LOG.debug('Zone Export too large to perform synchronously')
|
||||
export['status'] = 'ERROR'
|
||||
export['message'] = 'Zone is too large to export'
|
||||
return export
|
||||
|
||||
export['location'] = \
|
||||
"designate://v2/zones/tasks/exports/%(eid)s/export" % \
|
||||
{'eid': export['id']}
|
||||
|
||||
export['status'] = 'COMPLETE'
|
||||
else:
|
||||
LOG.debug('No method found to export zone')
|
||||
export['status'] = 'ERROR'
|
||||
export['message'] = 'No suitable method for export'
|
||||
|
||||
return export
|
||||
|
||||
def _export_zone(self, context, zone_id):
|
||||
domain = self.central_api.get_domain(context, zone_id)
|
||||
|
||||
criterion = {'domain_id': zone_id}
|
||||
recordsets = self.storage.find_recordsets_export(context, criterion)
|
||||
|
||||
return utils.render_template('export-zone.jinja2',
|
||||
domain=domain,
|
||||
recordsets=recordsets)
|
||||
|
@ -99,5 +99,4 @@ Admin API
|
||||
:glob:
|
||||
|
||||
rest/admin/quotas
|
||||
rest/admin/zones
|
||||
|
||||
|
@ -1,59 +0,0 @@
|
||||
Zones
|
||||
=====
|
||||
|
||||
Overview
|
||||
--------
|
||||
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
|
||||
added to the designate.conf file::
|
||||
|
||||
enabled_extensions_admin = zones
|
||||
|
||||
Once this line has been added, restart the designate-api service.
|
||||
|
||||
Export Zone
|
||||
-----------
|
||||
|
||||
.. http:get:: /admin/zones/export/(uuid:id)
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /admin/zones/export/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: text/dns
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/dns
|
||||
|
||||
$ORIGIN example.com.
|
||||
$TTL 42
|
||||
|
||||
example.com. IN SOA ns.designate.com. nsadmin.example.com. (
|
||||
1394213803 ; serial
|
||||
3600 ; refresh
|
||||
600 ; retry
|
||||
86400 ; expire
|
||||
3600 ; minimum
|
||||
)
|
||||
|
||||
|
||||
example.com. IN NS ns.designate.com.
|
||||
|
||||
|
||||
example.com. IN MX 10 mail.example.com.
|
||||
ns.example.com. IN A 10.0.0.1
|
||||
mail.example.com. IN A 10.0.0.2
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 406: Not Acceptable
|
||||
|
||||
Notice how the SOA and NS records are replaced with the Designate server(s).
|
@ -748,3 +748,232 @@ Delete Zone Import
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:statuscode 204: No Content
|
||||
|
||||
Export Zone
|
||||
-----------
|
||||
|
||||
Create a Zone Export
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:post:: /zones/(uuid:id)/tasks/export
|
||||
|
||||
To export a zone in BIND9 zonefile format, a zone export resource must be
|
||||
created. This is accomplished by initializing an export task.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v2/zones/074e805e-fe87-4cbb-b10b-21a06e215d41/tasks/export HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202 Accepted
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "PENDING",
|
||||
"zone_id": "074e805e-fe87-4cbb-b10b-21a06e215d41",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/exports/8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720"
|
||||
},
|
||||
"created_at": "2015-08-27T20:57:03.000000",
|
||||
"updated_at": null,
|
||||
"version": 1,
|
||||
"location": null,
|
||||
"message": null,
|
||||
"project_id": "1",
|
||||
"id": "8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720"
|
||||
}
|
||||
:statuscode 202: Accepted
|
||||
|
||||
View a Zone Export Record
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:get:: /zones/tasks/exports/(uuid:id)
|
||||
|
||||
The status of a zone export can be viewed by querying the id
|
||||
given when the request was created.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/tasks/exports/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/exports/8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720",
|
||||
"export": "http://127.0.0.1:9001/v2/zones/tasks/exports/8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720/export"
|
||||
},
|
||||
"created_at": "2015-08-27T20:57:03.000000",
|
||||
"updated_at": "2015-08-27T20:57:03.000000",
|
||||
"version": 2,
|
||||
"location": "designate://v2/zones/tasks/exports/8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720/export",
|
||||
"message": null,
|
||||
"project_id": "noauth-project",
|
||||
"id": "8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720"
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
:statuscode 404: Not Found
|
||||
|
||||
Notice the status has been updated and there is now an 'export' in the 'links' field that points
|
||||
to a link where the export (zonefile) can be accessed.
|
||||
|
||||
|
||||
View the Exported Zone
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The link that is generated in the export field in an export resource can be followed to
|
||||
a Designate resource, or an external resource. If the link is to a Designate endpoint, the
|
||||
zonefile can be retrieved directly through the API by following that link.
|
||||
|
||||
.. http:get:: /zones/tasks/exports/(uuid:id)
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /zones/tasks/exports/8ec17fe1-d1f9-41b4-aa98-4eeb4c27b720/export HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: text/dns
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/dns
|
||||
|
||||
$ORIGIN example.com.
|
||||
$TTL 42
|
||||
|
||||
example.com. IN SOA ns.designate.com. nsadmin.example.com. (
|
||||
1394213803 ; serial
|
||||
3600 ; refresh
|
||||
600 ; retry
|
||||
86400 ; expire
|
||||
3600 ; minimum
|
||||
)
|
||||
|
||||
|
||||
example.com. IN NS ns.designate.com.
|
||||
|
||||
|
||||
example.com. IN MX 10 mail.example.com.
|
||||
ns.example.com. IN A 10.0.0.1
|
||||
mail.example.com. IN A 10.0.0.2
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
:statuscode 404: Not Found
|
||||
|
||||
Notice how the SOA and NS records are replaced with the Designate server(s).
|
||||
|
||||
List Zone Exports
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:get:: /zones/tasks/exports/
|
||||
|
||||
List all of the zone exports created by this project.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/tasks/exports/ 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
|
||||
|
||||
{
|
||||
"exports": [
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"zone_id": "30ea7692-7f9e-4195-889e-0ba11620b491",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/exports/d2f36aa6-2da4-4b22-a2a9-9cdf19a2f248",
|
||||
"export": "http://127.0.0.1:9001/v2/zones/30ea7692-7f9e-4195-889e-0ba11620b491/tasks/exports/d2f36aa6-2da4-4b22-a2a9-9cdf19a2f248/export"
|
||||
},
|
||||
"created_at": "2015-08-24T19:46:50.000000",
|
||||
"updated_at": "2015-08-24T19:46:50.000000",
|
||||
"version": 2,
|
||||
"location": "designate://v2/zones/30ea7692-7f9e-4195-889e-0ba11620b491/tasks/exports/d2f36aa6-2da4-4b22-a2a9-9cdf19a2f248/export",
|
||||
"message": null,
|
||||
"project_id": "noauth-project",
|
||||
"id": "d2f36aa6-2da4-4b22-a2a9-9cdf19a2f248"
|
||||
},
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"zone_id": "0503f9fd-3938-47a4-bbf3-df99b088abfc",
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/exports/3d7d07a5-2ce3-458e-b3dd-6a29906234d8",
|
||||
"export": "http://127.0.0.1:9001/v2/zones/tasks/exports/3d7d07a5-2ce3-458e-b3dd-6a29906234d8/export"
|
||||
},
|
||||
"created_at": "2015-08-25T15:16:10.000000",
|
||||
"updated_at": "2015-08-25T15:16:10.000000",
|
||||
"version": 2,
|
||||
"location": "designate://v2/zones/tasks/exports/3d7d07a5-2ce3-458e-b3dd-6a29906234d8/export",
|
||||
"message": null,
|
||||
"project_id": "noauth-project",
|
||||
"id": "3d7d07a5-2ce3-458e-b3dd-6a29906234d8"
|
||||
},
|
||||
],
|
||||
"links": {
|
||||
"self": "http://127.0.0.1:9001/v2/zones/tasks/exports"
|
||||
}
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
:statuscode 404: Not Found
|
||||
|
||||
Delete Zone Export
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. http:delete:: /zones/tasks/exports/(uuid:id)
|
||||
|
||||
Deletes a zone export with the specified ID. This does not affect the zone
|
||||
that was exported, it simply removes the record of the export. If the link
|
||||
to view the export was pointing to a Designate API endpoint, the endpoint
|
||||
will no longer be available.
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v2/zones/tasks/exports/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
|
@ -216,6 +216,9 @@ debug = False
|
||||
# Can be one or more of: periodic_exists
|
||||
#enabled_tasks = None
|
||||
|
||||
# Whether to allow synchronous zone exports
|
||||
#export_synchronous = True
|
||||
|
||||
#-----------------------
|
||||
# Pool Manager Service
|
||||
#-----------------------
|
||||
|
@ -108,11 +108,16 @@
|
||||
"update_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"
|
||||
"delete_zone_import": "rule:admin_or_owner",
|
||||
|
||||
"zone_export": "rule:admin_or_owner",
|
||||
"create_zone_export": "rule:admin_or_owner",
|
||||
"find_zone_exports": "rule:admin_or_owner",
|
||||
"get_zone_export": "rule:admin_or_owner",
|
||||
"update_zone_export": "rule:admin_or_owner",
|
||||
"delete_zone_export": "rule:admin_or_owner"
|
||||
}
|
||||
|
71
functionaltests/api/v2/clients/zone_export_client.py
Normal file
71
functionaltests/api/v2/clients/zone_export_client.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""
|
||||
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_export_model import ZoneExportModel
|
||||
from functionaltests.api.v2.models.zone_export_model import ZoneExportListModel
|
||||
from functionaltests.common.client import ClientMixin
|
||||
from functionaltests.common import utils
|
||||
|
||||
|
||||
class ZoneExportClient(ClientMixin):
|
||||
|
||||
@classmethod
|
||||
def zone_exports_uri(cls, filters=None):
|
||||
url = "/v2/zones/tasks/exports"
|
||||
if filters:
|
||||
url = cls.add_filters(url, filters)
|
||||
return url
|
||||
|
||||
@classmethod
|
||||
def zone_export_uri(cls, id):
|
||||
return "{0}/{1}".format(cls.zone_exports_uri(), id)
|
||||
|
||||
def list_zone_exports(self, filters=None, **kwargs):
|
||||
resp, body = self.client.get(
|
||||
self.zone_exports_uri(filters), **kwargs)
|
||||
return self.deserialize(resp, body, ZoneExportListModel)
|
||||
|
||||
def get_zone_export(self, id, **kwargs):
|
||||
resp, body = self.client.get(self.zone_export_uri(id))
|
||||
return self.deserialize(resp, body, ZoneExportModel)
|
||||
|
||||
def get_exported_zone(self, id, **kwargs):
|
||||
uri = "/v2/zones/tasks/exports/{0}".format(id)
|
||||
resp, body = self.client.get(uri)
|
||||
return resp, body
|
||||
|
||||
def post_zone_export(self, zone_id, **kwargs):
|
||||
uri = "/v2/zones/{0}/tasks/export".format(zone_id)
|
||||
|
||||
resp, body = self.client.post(uri, body='', **kwargs)
|
||||
return self.deserialize(resp, body, ZoneExportModel)
|
||||
|
||||
def delete_zone_export(self, id, **kwargs):
|
||||
resp, body = self.client.delete(self.zone_export_uri(id), **kwargs)
|
||||
return resp, body
|
||||
|
||||
def wait_for_zone_export(self, zone_export_id):
|
||||
utils.wait_for_condition(
|
||||
lambda: self.is_zone_export_active(zone_export_id))
|
||||
|
||||
def is_zone_export_active(self, zone_export_id):
|
||||
resp, model = self.get_zone_export(zone_export_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_export_model.py
Normal file
27
functionaltests/api/v2/models/zone_export_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 ZoneExportModel(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ZoneExportListModel(CollectionModel):
|
||||
COLLECTION_NAME = 'exports'
|
||||
MODEL_TYPE = ZoneExportModel
|
@ -22,6 +22,7 @@ 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
|
||||
from functionaltests.api.v2.clients.zone_export_client import ZoneExportClient
|
||||
|
||||
|
||||
class ZoneTest(DesignateV2Test):
|
||||
@ -140,3 +141,35 @@ class ZoneImportTest(DesignateV2Test):
|
||||
import_client.delete_zone_import(import_id)
|
||||
self.assertRaises(NotFound,
|
||||
lambda: import_client.get_zone_import(model.id))
|
||||
|
||||
|
||||
class ZoneExportTest(DesignateV2Test):
|
||||
|
||||
def setUp(self):
|
||||
super(ZoneExportTest, self).setUp()
|
||||
|
||||
def test_import_domain(self):
|
||||
user = 'default'
|
||||
resp, zone = ZoneClient.as_user(user).post_zone(
|
||||
datagen.random_zone_data())
|
||||
ZoneClient.as_user(user).wait_for_zone(zone.id)
|
||||
|
||||
export_client = ZoneExportClient.as_user(user)
|
||||
|
||||
resp, model = export_client.post_zone_export(zone.id)
|
||||
|
||||
export_id = model.id
|
||||
self.assertEqual(resp.status, 202)
|
||||
self.assertEqual(model.status, 'PENDING')
|
||||
export_client.wait_for_zone_export(export_id)
|
||||
|
||||
resp, model = export_client.get_zone_export(export_id)
|
||||
self.assertEqual(resp.status, 200)
|
||||
self.assertEqual(model.status, 'COMPLETE')
|
||||
|
||||
resp, body = export_client.get_exported_zone(export_id)
|
||||
self.assertEqual(resp.status, 200)
|
||||
|
||||
export_client.delete_zone_export(export_id)
|
||||
self.assertRaises(NotFound,
|
||||
lambda: export_client.get_zone_export(model.id))
|
||||
|
Loading…
Reference in New Issue
Block a user