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('mdns-topic', default='mdns', help='mDNS Topic'),
|
||||||
cfg.StrOpt('pool-manager-topic', default='pool_manager',
|
cfg.StrOpt('pool-manager-topic', default='pool_manager',
|
||||||
help='Pool Manager Topic'),
|
help='Pool Manager Topic'),
|
||||||
|
cfg.StrOpt('zone-manager-topic', default='zone_manager',
|
||||||
|
help='Zone Manager Topic'),
|
||||||
|
|
||||||
# Default TTL
|
# Default TTL
|
||||||
cfg.IntOpt('default-ttl', default=3600),
|
cfg.IntOpt('default-ttl', default=3600),
|
||||||
|
@ -33,6 +33,7 @@ from oslo_log import log as logging
|
|||||||
|
|
||||||
from designate import exceptions
|
from designate import exceptions
|
||||||
from designate.central import rpcapi as central_rpcapi
|
from designate.central import rpcapi as central_rpcapi
|
||||||
|
from designate.zone_manager import rpcapi as zone_manager_rpcapi
|
||||||
from designate.i18n import _
|
from designate.i18n import _
|
||||||
|
|
||||||
|
|
||||||
@ -54,6 +55,10 @@ class RestController(pecan.rest.RestController):
|
|||||||
def central_api(self):
|
def central_api(self):
|
||||||
return central_rpcapi.CentralAPI.get_instance()
|
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):
|
def _apply_filter_params(self, params, accepted_filters, criterion):
|
||||||
invalid=[]
|
invalid=[]
|
||||||
for k in params:
|
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.xfr import XfrController
|
||||||
from designate.api.v2.controllers.zones.tasks.imports \
|
from designate.api.v2.controllers.zones.tasks.imports \
|
||||||
import ZoneImportController
|
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
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -36,3 +40,5 @@ class TasksController(object):
|
|||||||
abandon = abandon.AbandonController()
|
abandon = abandon.AbandonController()
|
||||||
xfr = XfrController()
|
xfr = XfrController()
|
||||||
imports = ZoneImportController()
|
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.1 - Add xfr_domain
|
||||||
5.2 - Add Zone Import methods
|
5.2 - Add Zone Import methods
|
||||||
5.3 - Add Zone Export method
|
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):
|
def __init__(self, topic=None):
|
||||||
topic = topic if topic else cfg.CONF.central_topic
|
topic = topic if topic else cfg.CONF.central_topic
|
||||||
|
|
||||||
target = messaging.Target(topic=topic, version=self.RPC_API_VERSION)
|
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
|
@classmethod
|
||||||
def get_instance(cls):
|
def get_instance(cls):
|
||||||
@ -535,3 +536,36 @@ class CentralAPI(object):
|
|||||||
"delete_zone_import."))
|
"delete_zone_import."))
|
||||||
return self.client.call(context, 'delete_zone_import',
|
return self.client.call(context, 'delete_zone_import',
|
||||||
zone_import_id=zone_import_id)
|
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 import storage
|
||||||
from designate.mdns import rpcapi as mdns_rpcapi
|
from designate.mdns import rpcapi as mdns_rpcapi
|
||||||
from designate.pool_manager import rpcapi as pool_manager_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__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -259,7 +260,7 @@ def notification(notification_type):
|
|||||||
|
|
||||||
|
|
||||||
class Service(service.RPCService, service.Service):
|
class Service(service.RPCService, service.Service):
|
||||||
RPC_API_VERSION = '5.3'
|
RPC_API_VERSION = '5.4'
|
||||||
|
|
||||||
target = messaging.Target(version=RPC_API_VERSION)
|
target = messaging.Target(version=RPC_API_VERSION)
|
||||||
|
|
||||||
@ -307,6 +308,10 @@ class Service(service.RPCService, service.Service):
|
|||||||
def pool_manager_api(self):
|
def pool_manager_api(self):
|
||||||
return pool_manager_rpcapi.PoolManagerAPI.get_instance()
|
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):
|
def _is_valid_domain_name(self, context, domain_name):
|
||||||
# Validate domain name length
|
# Validate domain name length
|
||||||
if len(domain_name) > cfg.CONF['service:central'].max_domain_name_len:
|
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)
|
zone_import = self.storage.delete_zone_import(context, zone_import_id)
|
||||||
|
|
||||||
return zone_import
|
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'
|
error_type = 'duplicate_zone_import'
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateZoneExport(Duplicate):
|
||||||
|
error_type = 'duplicate_zone_export'
|
||||||
|
|
||||||
|
|
||||||
class MethodNotAllowed(Base):
|
class MethodNotAllowed(Base):
|
||||||
expected = True
|
expected = True
|
||||||
error_code = 405
|
error_code = 405
|
||||||
@ -382,6 +386,10 @@ class ZoneImportNotFound(NotFound):
|
|||||||
error_type = 'zone_import_not_found'
|
error_type = 'zone_import_not_found'
|
||||||
|
|
||||||
|
|
||||||
|
class ZoneExportNotFound(NotFound):
|
||||||
|
error_type = 'zone_export_not_found'
|
||||||
|
|
||||||
|
|
||||||
class LastServerDeleteNotAllowed(BadRequest):
|
class LastServerDeleteNotAllowed(BadRequest):
|
||||||
error_type = 'last_server_delete_not_allowed'
|
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_request import ZoneTransferRequest, ZoneTransferRequestList # noqa
|
||||||
from designate.objects.zone_transfer_accept import ZoneTransferAccept, ZoneTransferAcceptList # noqa
|
from designate.objects.zone_transfer_accept import ZoneTransferAccept, ZoneTransferAcceptList # noqa
|
||||||
from designate.objects.zone_import import ZoneImport, ZoneImportList # noqa
|
from designate.objects.zone_import import ZoneImport, ZoneImportList # noqa
|
||||||
|
from designate.objects.zone_export import ZoneExport, ZoneExportList # noqa
|
||||||
|
|
||||||
# Record Types
|
# 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.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.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_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'),
|
help='Number of records allowed per domain'),
|
||||||
cfg.IntOpt('quota-recordset-records', default=20,
|
cfg.IntOpt('quota-recordset-records', default=20,
|
||||||
help='Number of records allowed per recordset'),
|
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_recordsets': cfg.CONF.quota_domain_recordsets,
|
||||||
'domain_records': cfg.CONF.quota_domain_records,
|
'domain_records': cfg.CONF.quota_domain_records,
|
||||||
'recordset_records': cfg.CONF.quota_recordset_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):
|
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
|
: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):
|
def ping(self, context):
|
||||||
"""Ping the Storage connection"""
|
"""Ping the Storage connection"""
|
||||||
return {
|
return {
|
||||||
|
@ -1299,6 +1299,46 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
|||||||
return self._delete(context, tables.zone_tasks, zone_import,
|
return self._delete(context, tables.zone_tasks, zone_import,
|
||||||
exceptions.ZoneImportNotFound)
|
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
|
# diagnostics
|
||||||
def ping(self, context):
|
def ping(self, context):
|
||||||
start_time = time.time()
|
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_ATTRIBUTE_KEYS = ('master',)
|
||||||
|
|
||||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
||||||
ZONE_TASK_TYPES = ['IMPORT']
|
ZONE_TASK_TYPES = ['IMPORT', 'EXPORT']
|
||||||
|
|
||||||
|
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
@ -334,6 +334,7 @@ zone_tasks = Table('zone_tasks', metadata,
|
|||||||
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
|
Column('status', Enum(name='resource_statuses', *TASK_STATUSES),
|
||||||
nullable=False, server_default='ACTIVE',
|
nullable=False, server_default='ACTIVE',
|
||||||
default='ACTIVE'),
|
default='ACTIVE'),
|
||||||
|
Column('location', String(160), nullable=True),
|
||||||
|
|
||||||
mysql_engine='INNODB',
|
mysql_engine='INNODB',
|
||||||
mysql_charset='utf8')
|
mysql_charset='utf8')
|
||||||
|
@ -44,6 +44,7 @@ class QuotaTestCase(tests.TestCase):
|
|||||||
|
|
||||||
self.assertIsNotNone(quotas)
|
self.assertIsNotNone(quotas)
|
||||||
self.assertEqual({
|
self.assertEqual({
|
||||||
|
'api_export_size': cfg.CONF.quota_api_export_size,
|
||||||
'domains': cfg.CONF.quota_domains,
|
'domains': cfg.CONF.quota_domains,
|
||||||
'domain_recordsets': cfg.CONF.quota_domain_recordsets,
|
'domain_recordsets': cfg.CONF.quota_domain_recordsets,
|
||||||
'domain_records': cfg.CONF.quota_domain_records,
|
'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.',
|
r = self.service._is_subdomain(self.context, 'foo.a.b.example.com.',
|
||||||
'1')
|
'1')
|
||||||
self.assertEqual(r, 'example.com.')
|
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,
|
cfg.IntOpt('threads', default=1000,
|
||||||
help='Number of Zone Manager greenthreads to spawn'),
|
help='Number of Zone Manager greenthreads to spawn'),
|
||||||
cfg.ListOpt('enabled_tasks', default=None,
|
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')
|
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.
|
# under the License.
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
import oslo_messaging as messaging
|
||||||
|
|
||||||
from designate.i18n import _LI
|
from designate.i18n import _LI
|
||||||
from designate import coordination
|
from designate import coordination
|
||||||
|
from designate import exceptions
|
||||||
|
from designate import quota
|
||||||
from designate import service
|
from designate import service
|
||||||
|
from designate import storage
|
||||||
|
from designate import utils
|
||||||
|
from designate.central import rpcapi
|
||||||
from designate.zone_manager import tasks
|
from designate.zone_manager import tasks
|
||||||
|
|
||||||
|
|
||||||
@ -28,11 +34,29 @@ CONF = cfg.CONF
|
|||||||
NS = 'designate.periodic_tasks'
|
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
|
@property
|
||||||
def service_name(self):
|
def service_name(self):
|
||||||
return 'zone_manager'
|
return 'zone_manager'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def central_api(self):
|
||||||
|
return rpcapi.CentralAPI.get_instance()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
super(Service, self).start()
|
super(Service, self).start()
|
||||||
|
|
||||||
@ -59,3 +83,67 @@ class Service(coordination.CoordinationMixin, service.Service):
|
|||||||
def _rebalance(self, my_partitions, members, event):
|
def _rebalance(self, my_partitions, members, event):
|
||||||
LOG.info(_LI("Received rebalance event %s") % event)
|
LOG.info(_LI("Received rebalance event %s") % event)
|
||||||
self.partition_range = my_partitions
|
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:
|
:glob:
|
||||||
|
|
||||||
rest/admin/quotas
|
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
|
HTTP/1.1 204 No Content
|
||||||
|
|
||||||
:statuscode 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
|
# Can be one or more of: periodic_exists
|
||||||
#enabled_tasks = None
|
#enabled_tasks = None
|
||||||
|
|
||||||
|
# Whether to allow synchronous zone exports
|
||||||
|
#export_synchronous = True
|
||||||
|
|
||||||
#-----------------------
|
#-----------------------
|
||||||
# Pool Manager Service
|
# Pool Manager Service
|
||||||
#-----------------------
|
#-----------------------
|
||||||
|
@ -108,11 +108,16 @@
|
|||||||
"update_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",
|
"create_zone_import": "rule:admin_or_owner",
|
||||||
"find_zone_imports": "rule:admin_or_owner",
|
"find_zone_imports": "rule:admin_or_owner",
|
||||||
"get_zone_import": "rule:admin_or_owner",
|
"get_zone_import": "rule:admin_or_owner",
|
||||||
"update_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.base import DesignateV2Test
|
||||||
from functionaltests.api.v2.clients.zone_client import ZoneClient
|
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_import_client import ZoneImportClient
|
||||||
|
from functionaltests.api.v2.clients.zone_export_client import ZoneExportClient
|
||||||
|
|
||||||
|
|
||||||
class ZoneTest(DesignateV2Test):
|
class ZoneTest(DesignateV2Test):
|
||||||
@ -140,3 +141,35 @@ class ZoneImportTest(DesignateV2Test):
|
|||||||
import_client.delete_zone_import(import_id)
|
import_client.delete_zone_import(import_id)
|
||||||
self.assertRaises(NotFound,
|
self.assertRaises(NotFound,
|
||||||
lambda: import_client.get_zone_import(model.id))
|
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