Add new API for zone move
The new API would be v2/zones/<zone_id>/tasks/move Only POST would be allowed on this API. This move zone from existing pool and add it in new pool. After zone pool_id field will be updated in DB, clone-zone will be created on target pool backend servers. The zone transfer(AXFR/IXFR) will happen and the zone on target pool gets synced with the Designate DB. This command serve as replacement to "zone export + zone delete + zone import" procedure. Added following things in pool move operation: - Add/Update NS servers of new pool in the zone Implements: blueprint zone-move Change-Id: I5307de429114b20efd9785c3c0cdb33977418423
This commit is contained in:
parent
f5a034272d
commit
8733f8f85b
@ -46,7 +46,6 @@ Request
|
||||
- zone_id: path_zone_id
|
||||
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
@ -102,3 +101,57 @@ Response Parameters
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- x-openstack-request-id: x-openstack-request-id
|
||||
|
||||
|
||||
|
||||
Pool Move Zone
|
||||
==============
|
||||
|
||||
.. rest_method:: POST /v2/zones/{zone_id}/tasks/pool_move
|
||||
|
||||
Move a zone to another pool.
|
||||
|
||||
This moves a zone from the existing designate pool to specified target pool. If
|
||||
pool is not specified by admin, designate will determine suitable pool by
|
||||
itself and move zone to that pool.
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 202
|
||||
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 400
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
- 405
|
||||
- 500
|
||||
- 503
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- x-auth-token: x-auth-token
|
||||
- x-auth-all-projects: x-auth-all-projects
|
||||
- x-auth-sudo-project-id: x-auth-sudo-project-id
|
||||
- zone_id: path_zone_id
|
||||
- pool_id: zone_pool_target_id
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: samples/zones/poolmove-zone-request.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- x-openstack-request-id: x-openstack-request-id
|
||||
|
@ -953,6 +953,13 @@ zone_pool_id:
|
||||
required: true
|
||||
type: uuid
|
||||
|
||||
zone_pool_target_id:
|
||||
description: |
|
||||
The target pool ID to move the zone into
|
||||
in: body
|
||||
required: false
|
||||
type: uuid
|
||||
|
||||
zone_serial:
|
||||
description: |
|
||||
current serial number for the zone
|
||||
|
3
api-ref/source/samples/zones/poolmove-zone-request.json
Normal file
3
api-ref/source/samples/zones/poolmove-zone-request.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"pool_id": "8e6f1c59-15e7-4a14-8640-8d5e07f95b10"
|
||||
}
|
@ -22,6 +22,8 @@ from designate.api.v2.controllers.zones.tasks.exports import (
|
||||
ZoneExportsController)
|
||||
from designate.api.v2.controllers.zones.tasks.imports import (
|
||||
ZoneImportController)
|
||||
from designate.api.v2.controllers.zones.tasks.pool_move import (
|
||||
PoolMoveController)
|
||||
from designate.api.v2.controllers.zones.tasks.transfer_accepts import (
|
||||
TransferAcceptsController as TRA)
|
||||
from designate.api.v2.controllers.zones.tasks.transfer_requests import (
|
||||
@ -37,6 +39,7 @@ class TasksController:
|
||||
transfer_requests = TRC()
|
||||
abandon = abandon.AbandonController()
|
||||
xfr = XfrController()
|
||||
pool_move = PoolMoveController()
|
||||
imports = ZoneImportController()
|
||||
exports = ZoneExportsController()
|
||||
export = ZoneExportCreateController()
|
||||
|
64
designate/api/v2/controllers/zones/tasks/pool_move.py
Normal file
64
designate/api/v2/controllers/zones/tasks/pool_move.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright 2022 Cloudification GmbH
|
||||
#
|
||||
# Author: Kiran P <contact@cloudification.io>
|
||||
#
|
||||
# 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
|
||||
import pecan
|
||||
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate import exceptions
|
||||
from designate.objects.adapters import DesignateAdapter
|
||||
from designate import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PoolMoveController(rest.RestController):
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@pecan.expose(template='json:', content_type='application/json-patch+json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def post_all(self, zone_id):
|
||||
"""Move a zone"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
body = request.body_dict
|
||||
context = request.environ['context']
|
||||
|
||||
zone = self.central_api.get_zone(context, zone_id)
|
||||
|
||||
if zone.action == "DELETE":
|
||||
raise exceptions.BadRequest('Can not move a deleting zone')
|
||||
|
||||
target_pool_id = None
|
||||
if 'pool_id' in body:
|
||||
if zone.pool_id == body['pool_id']:
|
||||
raise exceptions.BadRequest(
|
||||
'Target pool must be different for zone pool move')
|
||||
target_pool_id = body['pool_id']
|
||||
|
||||
# Update the zone object with the new values
|
||||
zone = DesignateAdapter.parse('API_v2', body, zone)
|
||||
zone.validate()
|
||||
|
||||
LOG.info("Triggered pool move for %(zone)s", {'zone': zone})
|
||||
zone = self.central_api.pool_move_zone(
|
||||
context, zone_id, target_pool_id)
|
||||
if zone.status == 'PENDING':
|
||||
response.status_int = 202
|
||||
else:
|
||||
response.status_int = 500
|
||||
|
||||
return ''
|
@ -72,8 +72,9 @@ class CentralAPI:
|
||||
6.7 - Add increment_zone_serial
|
||||
6.8 - Add managed recordset methods
|
||||
6.9 - Removed unused methods
|
||||
6.10 - Add Zone Pool Move method
|
||||
"""
|
||||
RPC_API_VERSION = '6.9'
|
||||
RPC_API_VERSION = '6.10'
|
||||
|
||||
# This allows us to mark some methods as not logged.
|
||||
# This can be for a few reasons - some methods my not actually call over
|
||||
@ -86,7 +87,7 @@ class CentralAPI:
|
||||
|
||||
target = messaging.Target(topic=self.topic,
|
||||
version=self.RPC_API_VERSION)
|
||||
self.client = rpc.get_client(target, version_cap='6.9')
|
||||
self.client = rpc.get_client(target, version_cap='6.10')
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
@ -175,6 +176,11 @@ class CentralAPI:
|
||||
return self.client.call(context, 'purge_zones',
|
||||
criterion=criterion, limit=limit)
|
||||
|
||||
def pool_move_zone(self, context, zone_id, target_pool_id):
|
||||
return self.client.call(context, 'pool_move_zone',
|
||||
zone_id=zone_id,
|
||||
target_pool_id=target_pool_id)
|
||||
|
||||
# Shared Zone methods
|
||||
def share_zone(self, context, zone_id, shared_zone):
|
||||
return self.client.call(context, 'share_zone', zone_id=zone_id,
|
||||
|
@ -54,7 +54,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Service(service.RPCService):
|
||||
RPC_API_VERSION = '6.9'
|
||||
RPC_API_VERSION = '6.10'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -1329,6 +1329,81 @@ class Service(service.RPCService):
|
||||
"Could not find %s" % zone.obj_name())
|
||||
return zone_shared
|
||||
|
||||
@rpc.expected_exceptions()
|
||||
@notification.notify_type('dns.domain.update')
|
||||
@notification.notify_type('dns.zone.update')
|
||||
def pool_move_zone(self, context, zone_id, target_pool_id=None):
|
||||
"""Move zone. Perform checks and then create zone in destination pool
|
||||
|
||||
:returns: moved zone
|
||||
"""
|
||||
if policy.enforce_new_defaults():
|
||||
target = {
|
||||
'zone_id': zone_id,
|
||||
constants.RBAC_PROJECT_ID: context.project_id,
|
||||
}
|
||||
else:
|
||||
target = {
|
||||
'zone_id': zone_id,
|
||||
'tenant_id': context.project_id,
|
||||
}
|
||||
|
||||
policy.check('pool_move_zone', context, target)
|
||||
|
||||
# Get the destination pool
|
||||
zone = self.storage.get_zone(context, zone_id)
|
||||
orig_pool_id = zone.pool_id
|
||||
|
||||
if target_pool_id is None:
|
||||
target_pool_id = self.scheduler.schedule_zone(context, zone)
|
||||
if target_pool_id == orig_pool_id:
|
||||
raise exceptions.BadRequest('No valid pool selected')
|
||||
# Update the orignal zone with new pool_id
|
||||
zone.pool_id = target_pool_id
|
||||
|
||||
# Need elevated context to get the pool
|
||||
elevated_context = context.elevated(all_tenants=True)
|
||||
try:
|
||||
self.storage.get_pool(elevated_context, target_pool_id)
|
||||
except exceptions.PoolNotFound:
|
||||
raise exceptions.BadRequest('Target pool does not exist')
|
||||
|
||||
target_pool_ns_records = self._get_pool_ns_records(context,
|
||||
target_pool_id)
|
||||
if len(target_pool_ns_records) == 0:
|
||||
LOG.critical('No nameservers configured. Please create at least '
|
||||
'one nameserver on target pool')
|
||||
raise exceptions.NoServersConfigured()
|
||||
|
||||
orig_pool_ns_records = self._get_pool_ns_records(context,
|
||||
orig_pool_id)
|
||||
|
||||
target_ns = {n.hostname for n in target_pool_ns_records}
|
||||
orig_ns = {n.hostname for n in orig_pool_ns_records}
|
||||
create_ns = target_ns.difference(orig_ns)
|
||||
delete_ns = orig_ns.difference(target_ns)
|
||||
|
||||
# Update target NS servers for the zone
|
||||
for ns_record in create_ns:
|
||||
self._add_ns(elevated_context, zone, ns_record)
|
||||
|
||||
# Then handle the ns_records to delete
|
||||
for ns_record in delete_ns:
|
||||
self._delete_ns(elevated_context, zone, ns_record)
|
||||
|
||||
zone = self._update_zone_in_storage(
|
||||
context, zone, increment_serial=False)
|
||||
|
||||
LOG.info("Moving zone '%(zone)s' to pool '%(pool)s'",
|
||||
{'zone': zone.name, 'pool': target_pool_id})
|
||||
zone.pool_id = target_pool_id
|
||||
zone.refresh = self._generate_soa_refresh_interval()
|
||||
zone.action = 'CREATE'
|
||||
zone.status = 'PENDING'
|
||||
self.worker_api.create_zone(context, zone)
|
||||
|
||||
return zone
|
||||
|
||||
# RecordSet Methods
|
||||
@rpc.expected_exceptions()
|
||||
@notification.notify_type('dns.recordset.create')
|
||||
|
@ -101,6 +101,12 @@ deprecated_purge_zones = policy.DeprecatedRule(
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since=versionutils.deprecated.WALLABY
|
||||
)
|
||||
deprecated_pool_move_zone = policy.DeprecatedRule(
|
||||
name="pool_move_zone",
|
||||
check_str=base.RULE_ADMIN,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since=versionutils.deprecated.WALLABY
|
||||
)
|
||||
|
||||
|
||||
rules = [
|
||||
@ -238,6 +244,19 @@ rules = [
|
||||
scope_types=[constants.PROJECT],
|
||||
deprecated_rule=deprecated_purge_zones
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name="pool_move_zone",
|
||||
check_str=base.SYSTEM_ADMIN,
|
||||
scope_types=[constants.PROJECT],
|
||||
description="Pool Move Zone",
|
||||
operations=[
|
||||
{
|
||||
'path': '/v2/zones/{zone_id}/tasks/pool_move',
|
||||
'method': 'POST'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_pool_move_zone,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
@ -20,7 +20,9 @@ class ZoneAPIv2Adapter(base.APIv2Adapter):
|
||||
MODIFICATIONS = {
|
||||
'fields': {
|
||||
"id": {},
|
||||
"pool_id": {},
|
||||
"pool_id": {
|
||||
'read_only': False
|
||||
},
|
||||
"project_id": {
|
||||
'rename': 'tenant_id'
|
||||
},
|
||||
|
@ -517,6 +517,56 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
self._assert_exception('not_found', 404, self.client.get, url,
|
||||
headers={'X-Test-Role': 'member'})
|
||||
|
||||
def test_post_pool_zone_move_invalid_pool_id(self):
|
||||
zone = self.create_zone()
|
||||
body = {'pool_id': zone.pool_id}
|
||||
self._assert_exception('bad_request', 400, self.client.post_json,
|
||||
'/zones/%s/tasks/pool_move' % zone['id'],
|
||||
body, headers={'X-Test-Role': 'admin'})
|
||||
|
||||
def test_post_pool_zone_move_invalid_action(self):
|
||||
# Create a zone
|
||||
zone = self.create_zone()
|
||||
body = {'pool_id': '12345'}
|
||||
zone.action = 'DELETE'
|
||||
with mock.patch.object(central_service.Service, 'get_zone',
|
||||
return_value=zone):
|
||||
self._assert_exception('bad_request', 400,
|
||||
self.client.post_json,
|
||||
'/zones/%s/tasks/pool_move' % zone['id'],
|
||||
body, headers={'X-Test-Role': 'admin'})
|
||||
|
||||
def test_post_pool_zone_move_non_admin_user(self):
|
||||
# Create a zone
|
||||
zone = self.create_zone()
|
||||
body = {'pool_id': '12345'}
|
||||
self._assert_exception('forbidden', 403, self.client.post_json,
|
||||
'/zones/%s/tasks/pool_move' % zone['id'], body)
|
||||
|
||||
def test_post_pool_zone_move_admin_user_status_500(self):
|
||||
# Create a zone
|
||||
zone = self.create_zone()
|
||||
body = {'pool_id': '12345'}
|
||||
response = self.client.post_json(
|
||||
'/zones/%s/tasks/pool_move' % zone['id'],
|
||||
body, status=500, headers={'X-Test-Role': 'admin'})
|
||||
|
||||
# Check the headers are what we expect
|
||||
self.assertEqual(500, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
def test_post_pool_zone_move_admin_user_status_202(self):
|
||||
# Create a zone
|
||||
zone = self.create_zone()
|
||||
body = {'pool_id': '12345'}
|
||||
zone.status = 'PENDING'
|
||||
with mock.patch.object(central_service.Service, 'pool_move_zone',
|
||||
return_value=zone):
|
||||
response = self.client.post_json(
|
||||
'/zones/%s/tasks/pool_move' % zone['id'], body,
|
||||
headers={'X-Test-Role': 'admin'})
|
||||
self.assertEqual(202, response.status_int)
|
||||
|
||||
def test_get_zone_tasks(self):
|
||||
# This is an invalid endpoint - should return 404
|
||||
zone = self.create_zone()
|
||||
|
@ -4240,6 +4240,66 @@ class CentralServiceTest(designate.tests.TestCase):
|
||||
self.assertEqual(shared_zone.project_id,
|
||||
retrived_shared_zone.project_id)
|
||||
|
||||
def test_pool_move_zone(self):
|
||||
pool = self.create_pool(fixture=0)
|
||||
zone = self.create_zone(context=self.admin_context, pool_id=pool.id)
|
||||
|
||||
# create second pool
|
||||
second_pool = self.create_pool(fixture=1)
|
||||
new_ns_record = objects.PoolNsRecord(hostname='ns-new.example.org.')
|
||||
second_pool.ns_records.append(new_ns_record)
|
||||
|
||||
moved_zone = self.central_service.pool_move_zone(
|
||||
self.admin_context,
|
||||
zone.id, second_pool['id'])
|
||||
self.assertEqual(zone.id, moved_zone.id)
|
||||
self.assertEqual(moved_zone.pool_id, second_pool['id'])
|
||||
|
||||
def test_pool_move_zone_without_target_pool(self):
|
||||
pool = self.create_pool(fixture=0)
|
||||
zone = self.create_zone(context=self.admin_context, pool_id=pool.id)
|
||||
|
||||
# create second pool
|
||||
second_pool = self.create_pool(fixture=1)
|
||||
new_ns_record = objects.PoolNsRecord(hostname='ns-new.example.org.')
|
||||
second_pool.ns_records.append(new_ns_record)
|
||||
|
||||
zone.pool_id = None
|
||||
with mock.patch.object(self.central_service.scheduler, 'schedule_zone',
|
||||
return_value=second_pool['id']):
|
||||
moved_zone = self.central_service.pool_move_zone(
|
||||
self.admin_context,
|
||||
zone.id)
|
||||
self.assertEqual(zone.id, moved_zone.id)
|
||||
self.assertEqual(moved_zone.pool_id, second_pool['id'])
|
||||
|
||||
def test_pool_move_zone_exception_no_ns_records(self):
|
||||
pool = self.create_pool(fixture=0)
|
||||
zone = self.create_zone(context=self.admin_context, pool_id=pool.id)
|
||||
|
||||
# create second pool
|
||||
second_pool = self.create_pool(fixture=1)
|
||||
|
||||
zone.pool_id = second_pool['id']
|
||||
with mock.patch.object(self.central_service, '_get_pool_ns_records',
|
||||
return_value=[]):
|
||||
self.assertRaises(exceptions.NoServersConfigured,
|
||||
self.central_service.pool_move_zone,
|
||||
self.admin_context,
|
||||
zone.id, second_pool['id'])
|
||||
|
||||
def test_pool_move_zone_exception_invalid_pool_id(self):
|
||||
pool = self.create_pool(fixture=0)
|
||||
zone = self.create_zone(context=self.admin_context, pool_id=pool.id)
|
||||
|
||||
# Use fake pool ID
|
||||
pool_id = '521935cf-d5be-44a2-9f64-fb5a316a61d2'
|
||||
exc = self.assertRaises(rpc_dispatcher.ExpectedException,
|
||||
self.central_service.pool_move_zone,
|
||||
self.admin_context,
|
||||
zone.id, target_pool_id=pool_id)
|
||||
self.assertEqual(exceptions.BadRequest, exc.exc_info[0])
|
||||
|
||||
def test_create_managed_records(self):
|
||||
zone = self.create_zone()
|
||||
|
||||
|
10
releasenotes/notes/zone-pool-move-7bb8e1f0839c3c0d.yaml
Normal file
10
releasenotes/notes/zone-pool-move-7bb8e1f0839c3c0d.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added zone pool move command which allows admin user to move zone from
|
||||
pool A to specified pool B. This command overcome the issues observed in
|
||||
zone export-import thereby reducing hours of time of large zone imports
|
||||
(e.g. 20-30k records). Please note, if you have moved a zone to a
|
||||
different pool, the pool must be configured with a proper tsig key for
|
||||
mini-DNS query operations. Without this, you cannot have overlapping zones
|
||||
in different pools.
|
Loading…
Reference in New Issue
Block a user