Merge "Add a new API for abandoning a zone from storage"
This commit is contained in:
commit
86dddab7e3
@ -248,14 +248,8 @@ class ZonesController(rest.RestController):
|
|||||||
response = pecan.response
|
response = pecan.response
|
||||||
context = request.environ['context']
|
context = request.environ['context']
|
||||||
|
|
||||||
# TODO(kiall): Validate we have a sane UUID for zone_id
|
self.central_api.delete_domain(context, zone_id)
|
||||||
|
response.status_int = 202
|
||||||
zone = self.central_api.delete_domain(context, zone_id)
|
|
||||||
|
|
||||||
if zone['status'] == 'DELETING':
|
|
||||||
response.status_int = 202
|
|
||||||
else:
|
|
||||||
response.status_int = 204
|
|
||||||
|
|
||||||
# NOTE: This is a hack and a half.. But Pecan needs it.
|
# NOTE: This is a hack and a half.. But Pecan needs it.
|
||||||
return ''
|
return ''
|
||||||
|
@ -20,6 +20,7 @@ from designate.api.v2.controllers.zones.tasks.transfer_requests \
|
|||||||
import TransferRequestsController as TRC
|
import TransferRequestsController as TRC
|
||||||
from designate.api.v2.controllers.zones.tasks.transfer_accepts \
|
from designate.api.v2.controllers.zones.tasks.transfer_accepts \
|
||||||
import TransferAcceptsController as TRA
|
import TransferAcceptsController as TRA
|
||||||
|
from designate.api.v2.controllers.zones.tasks import abandon
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,3 +29,4 @@ class TasksController(rest.RestController):
|
|||||||
|
|
||||||
transfer_accepts = TRA()
|
transfer_accepts = TRA()
|
||||||
transfer_requests = TRC()
|
transfer_requests = TRC()
|
||||||
|
abandon = abandon.AbandonController()
|
||||||
|
40
designate/api/v2/controllers/zones/tasks/abandon.py
Normal file
40
designate/api/v2/controllers/zones/tasks/abandon.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Copyright (c) 2015 Rackspace Hosting
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from designate import utils
|
||||||
|
from designate.api.v2.controllers import rest
|
||||||
|
|
||||||
|
|
||||||
|
class AbandonController(rest.RestController):
|
||||||
|
|
||||||
|
@pecan.expose(template='json:', content_type='application/json')
|
||||||
|
@utils.validate_uuid('zone_id')
|
||||||
|
def post_all(self, zone_id):
|
||||||
|
"""Abandon a zone"""
|
||||||
|
request = pecan.request
|
||||||
|
response = pecan.response
|
||||||
|
context = request.environ['context']
|
||||||
|
context.abandon = 'True'
|
||||||
|
|
||||||
|
# abandon the zone
|
||||||
|
zone = self.central_api.delete_domain(context, zone_id)
|
||||||
|
if zone.deleted_at:
|
||||||
|
response.status_int = 204
|
||||||
|
else:
|
||||||
|
response.status_int = 500
|
||||||
|
|
||||||
|
# NOTE: This is a hack and a half.. But Pecan needs it.
|
||||||
|
return ''
|
@ -39,6 +39,7 @@ class ZonesView(base_view.BaseView):
|
|||||||
"ttl": zone['ttl'],
|
"ttl": zone['ttl'],
|
||||||
"serial": zone['serial'],
|
"serial": zone['serial'],
|
||||||
"status": zone['status'],
|
"status": zone['status'],
|
||||||
|
"action": zone['action'],
|
||||||
"version": zone['version'],
|
"version": zone['version'],
|
||||||
"created_at": zone['created_at'],
|
"created_at": zone['created_at'],
|
||||||
"updated_at": zone['updated_at'],
|
"updated_at": zone['updated_at'],
|
||||||
|
@ -30,6 +30,7 @@ from oslo_concurrency import lockutils
|
|||||||
|
|
||||||
from designate.i18n import _LI
|
from designate.i18n import _LI
|
||||||
from designate.i18n import _LC
|
from designate.i18n import _LC
|
||||||
|
from designate.i18n import _LW
|
||||||
from designate import context as dcontext
|
from designate import context as dcontext
|
||||||
from designate import exceptions
|
from designate import exceptions
|
||||||
from designate import network_api
|
from designate import network_api
|
||||||
@ -914,7 +915,10 @@ class Service(service.RPCService):
|
|||||||
'tenant_id': domain.tenant_id
|
'tenant_id': domain.tenant_id
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.check('delete_domain', context, target)
|
if hasattr(context, 'abandon') and context.abandon:
|
||||||
|
policy.check('abandon_domain', context, target)
|
||||||
|
else:
|
||||||
|
policy.check('delete_domain', context, target)
|
||||||
|
|
||||||
# Prevent deletion of a zone which has child zones
|
# Prevent deletion of a zone which has child zones
|
||||||
criterion = {'parent_domain_id': domain_id}
|
criterion = {'parent_domain_id': domain_id}
|
||||||
@ -923,9 +927,12 @@ class Service(service.RPCService):
|
|||||||
raise exceptions.DomainHasSubdomain('Please delete any subdomains '
|
raise exceptions.DomainHasSubdomain('Please delete any subdomains '
|
||||||
'before deleting this domain')
|
'before deleting this domain')
|
||||||
|
|
||||||
domain = self._delete_domain_in_storage(context, domain)
|
if hasattr(context, 'abandon') and context.abandon:
|
||||||
|
LOG.info(_LW("Abandoning zone '%(zone)s'") % {'zone': domain.name})
|
||||||
self.pool_manager_api.delete_domain(context, domain)
|
domain = self.storage.delete_domain(context, domain.id)
|
||||||
|
else:
|
||||||
|
domain = self._delete_domain_in_storage(context, domain)
|
||||||
|
self.pool_manager_api.delete_domain(context, domain)
|
||||||
|
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
@ -1997,6 +2004,8 @@ class Service(service.RPCService):
|
|||||||
# used to indicate the domain has been deleted and not the deleted
|
# used to indicate the domain has been deleted and not the deleted
|
||||||
# column. The deleted column is needed for unique constraints.
|
# column. The deleted column is needed for unique constraints.
|
||||||
if deleted:
|
if deleted:
|
||||||
|
# TODO(vinod): Pass a domain to delete_domain rather than id so
|
||||||
|
# that the action, status and serial are updated correctly.
|
||||||
self.storage.delete_domain(context, domain.id)
|
self.storage.delete_domain(context, domain.id)
|
||||||
|
|
||||||
def _update_record_status(self, context, domain_id, status, serial):
|
def _update_record_status(self, context, domain_id, status, serial):
|
||||||
|
@ -29,12 +29,13 @@ LOG = logging.getLogger(__name__)
|
|||||||
class DesignateContext(context.RequestContext):
|
class DesignateContext(context.RequestContext):
|
||||||
|
|
||||||
_all_tenants = False
|
_all_tenants = False
|
||||||
|
_abandon = None
|
||||||
|
|
||||||
def __init__(self, auth_token=None, user=None, tenant=None, domain=None,
|
def __init__(self, auth_token=None, user=None, tenant=None, domain=None,
|
||||||
user_domain=None, project_domain=None, is_admin=False,
|
user_domain=None, project_domain=None, is_admin=False,
|
||||||
read_only=False, show_deleted=False, request_id=None,
|
read_only=False, show_deleted=False, request_id=None,
|
||||||
resource_uuid=None, roles=None, service_catalog=None,
|
resource_uuid=None, roles=None, service_catalog=None,
|
||||||
all_tenants=False, user_identity=None):
|
all_tenants=False, user_identity=None, abandon=None):
|
||||||
# NOTE: user_identity may be passed in, but will be silently dropped as
|
# NOTE: user_identity may be passed in, but will be silently dropped as
|
||||||
# it is a generated field based on several others.
|
# it is a generated field based on several others.
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ class DesignateContext(context.RequestContext):
|
|||||||
self.service_catalog = service_catalog
|
self.service_catalog = service_catalog
|
||||||
|
|
||||||
self.all_tenants = all_tenants
|
self.all_tenants = all_tenants
|
||||||
|
self.abandon = abandon
|
||||||
|
|
||||||
if not hasattr(local.store, 'context'):
|
if not hasattr(local.store, 'context'):
|
||||||
self.update_store()
|
self.update_store()
|
||||||
@ -75,6 +77,7 @@ class DesignateContext(context.RequestContext):
|
|||||||
'roles': self.roles,
|
'roles': self.roles,
|
||||||
'service_catalog': self.service_catalog,
|
'service_catalog': self.service_catalog,
|
||||||
'all_tenants': self.all_tenants,
|
'all_tenants': self.all_tenants,
|
||||||
|
'abandon': self.abandon,
|
||||||
})
|
})
|
||||||
|
|
||||||
return copy.deepcopy(d)
|
return copy.deepcopy(d)
|
||||||
@ -128,3 +131,13 @@ class DesignateContext(context.RequestContext):
|
|||||||
if value:
|
if value:
|
||||||
policy.check('all_tenants', self)
|
policy.check('all_tenants', self)
|
||||||
self._all_tenants = value
|
self._all_tenants = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abandon(self):
|
||||||
|
return self._abandon
|
||||||
|
|
||||||
|
@abandon.setter
|
||||||
|
def abandon(self, value):
|
||||||
|
if value:
|
||||||
|
policy.check('abandon_domain', self)
|
||||||
|
self._abandon = value
|
||||||
|
@ -61,7 +61,13 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Zone Status",
|
"description": "Zone Status",
|
||||||
"enum": ["ACTIVE", "PENDING", "DELETING", "ERROR"],
|
"enum": ["ACTIVE", "PENDING", "ERROR"],
|
||||||
|
"readOnly": true
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Zone Action",
|
||||||
|
"enum": ["CREATE", "DELETE", "UPDATE", "NONE"],
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
"serial": {
|
"serial": {
|
||||||
|
@ -297,6 +297,11 @@ class SQLAlchemy(object):
|
|||||||
obj.deleted = obj.id.replace('-', '')
|
obj.deleted = obj.id.replace('-', '')
|
||||||
obj.deleted_at = timeutils.utcnow()
|
obj.deleted_at = timeutils.utcnow()
|
||||||
|
|
||||||
|
# TODO(vinod): Change the action to be null
|
||||||
|
# update the action and status before deleting the object
|
||||||
|
obj.action = 'NONE'
|
||||||
|
obj.status = 'DELETED'
|
||||||
|
|
||||||
# NOTE(kiall): It should be impossible for a duplicate exception to
|
# NOTE(kiall): It should be impossible for a duplicate exception to
|
||||||
# be raised in this call, therefore, it is OK to pass
|
# be raised in this call, therefore, it is OK to pass
|
||||||
# in "None" as the exc_dup param.
|
# in "None" as the exc_dup param.
|
||||||
|
@ -252,7 +252,7 @@ class ApiV2RecordSetsTest(ApiV2TestCase):
|
|||||||
self.assertEqual(200, response.status_int)
|
self.assertEqual(200, response.status_int)
|
||||||
|
|
||||||
# now delete the domain and get the recordsets
|
# now delete the domain and get the recordsets
|
||||||
self.client.delete('/zones/%s' % zone['id'], status=204)
|
self.client.delete('/zones/%s' % zone['id'], status=202)
|
||||||
|
|
||||||
# Simulate the domain having been deleted on the backend
|
# Simulate the domain having been deleted on the backend
|
||||||
domain_serial = self.central_service.get_domain(
|
domain_serial = self.central_service.get_domain(
|
||||||
|
@ -334,7 +334,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
|||||||
def test_delete_zone(self):
|
def test_delete_zone(self):
|
||||||
zone = self.create_domain()
|
zone = self.create_domain()
|
||||||
|
|
||||||
self.client.delete('/zones/%s' % zone['id'], status=204)
|
self.client.delete('/zones/%s' % zone['id'], status=202)
|
||||||
|
|
||||||
def test_delete_zone_invalid_id(self):
|
def test_delete_zone_invalid_id(self):
|
||||||
self._assert_invalid_uuid(self.client.delete, '/zones/%s')
|
self._assert_invalid_uuid(self.client.delete, '/zones/%s')
|
||||||
@ -354,6 +354,18 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
|||||||
self._assert_exception('domain_not_found', 404, self.client.delete,
|
self._assert_exception('domain_not_found', 404, self.client.delete,
|
||||||
url)
|
url)
|
||||||
|
|
||||||
|
def test_abandon_zone(self):
|
||||||
|
zone = self.create_domain()
|
||||||
|
url = '/zones/%s/tasks/abandon' % zone.id
|
||||||
|
|
||||||
|
# Ensure that we get permission denied
|
||||||
|
self._assert_exception('forbidden', 403, self.client.post_json, url)
|
||||||
|
|
||||||
|
# Ensure that abandon zone succeeds with the right policy
|
||||||
|
self.policy({'abandon_domain': '@'})
|
||||||
|
response = self.client.post_json(url)
|
||||||
|
self.assertEqual(204, response.status_int)
|
||||||
|
|
||||||
# Zone import/export
|
# Zone import/export
|
||||||
def test_missing_origin(self):
|
def test_missing_origin(self):
|
||||||
fixture = self.get_zonefile_fixture(variant='noorigin')
|
fixture = self.get_zonefile_fixture(variant='noorigin')
|
||||||
|
@ -261,7 +261,9 @@ Delete Zone
|
|||||||
|
|
||||||
.. http:delete:: zones/(uuid:id)
|
.. http:delete:: zones/(uuid:id)
|
||||||
|
|
||||||
Deletes a zone with the specified zone ID.
|
Deletes a zone with the specified zone ID. Deleting a zone is asynchronous.
|
||||||
|
Once pool manager has deleted the zone from all the pool targets, the zone
|
||||||
|
is deleted from storage.
|
||||||
|
|
||||||
**Example Request:**
|
**Example Request:**
|
||||||
|
|
||||||
@ -276,9 +278,9 @@ Delete Zone
|
|||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
HTTP/1.1 202 Accepted
|
||||||
|
|
||||||
:statuscode 204: No content
|
:statuscode 202: Accepted
|
||||||
|
|
||||||
Import Zone
|
Import Zone
|
||||||
-----------
|
-----------
|
||||||
@ -380,6 +382,33 @@ Export Zone
|
|||||||
|
|
||||||
Notice how the SOA and NS records are replaced with the Designate server(s).
|
Notice how the SOA and NS records are replaced with the Designate server(s).
|
||||||
|
|
||||||
|
Abandon Zone
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. http:post:: /zones/(uuid:id)/tasks/abandon
|
||||||
|
|
||||||
|
When a zone is abandoned it removes the zone from Designate's storage.
|
||||||
|
There is no operation done on the pool targets. This is intended to be used
|
||||||
|
in the cases where Designate's storage is incorrect for whatever reason. By
|
||||||
|
default this is restricted by policy (abandon_domain) to admins.
|
||||||
|
|
||||||
|
**Example Request:**
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3/tasks/abandon 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
|
||||||
|
|
||||||
Transfer Zone
|
Transfer Zone
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"find_domain": "rule:admin_or_owner",
|
"find_domain": "rule:admin_or_owner",
|
||||||
"update_domain": "rule:admin_or_owner",
|
"update_domain": "rule:admin_or_owner",
|
||||||
"delete_domain": "rule:admin_or_owner",
|
"delete_domain": "rule:admin_or_owner",
|
||||||
|
"abandon_domain": "rule:admin",
|
||||||
"count_domains": "rule:admin_or_owner",
|
"count_domains": "rule:admin_or_owner",
|
||||||
"touch_domain": "rule:admin_or_owner",
|
"touch_domain": "rule:admin_or_owner",
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user