Add a new API for abandoning a zone from storage

Add a new API for abandoning of zones from storage.
The new API would be v2/zones/<zone_id>/tasks/abandon
Only POST would be allowed on this API.
This just removes it from storage and does not take
any action on the backends.
Recordsets would not have this feature. By default this is
restricted by policy to admins.

Deleting a zone returns 202. Abandoning returns 204.
Zone now displays action too.

Change-Id: I213b567d1b609670b7c659f6e9780bdac240c89b
Closes-Bug: 1413024
This commit is contained in:
Vinod Mangalpally 2015-01-28 13:39:31 -06:00
parent 04c3bc7be1
commit c3afc3f4bb
12 changed files with 131 additions and 19 deletions

View File

@ -248,14 +248,8 @@ class ZonesController(rest.RestController):
response = pecan.response
context = request.environ['context']
# TODO(kiall): Validate we have a sane UUID for zone_id
zone = self.central_api.delete_domain(context, zone_id)
if zone['status'] == 'DELETING':
response.status_int = 202
else:
response.status_int = 204
self.central_api.delete_domain(context, zone_id)
response.status_int = 202
# NOTE: This is a hack and a half.. But Pecan needs it.
return ''

View File

@ -20,6 +20,7 @@ from designate.api.v2.controllers.zones.tasks.transfer_requests \
import TransferRequestsController as TRC
from designate.api.v2.controllers.zones.tasks.transfer_accepts \
import TransferAcceptsController as TRA
from designate.api.v2.controllers.zones.tasks import abandon
LOG = logging.getLogger(__name__)
@ -28,3 +29,4 @@ class TasksController(rest.RestController):
transfer_accepts = TRA()
transfer_requests = TRC()
abandon = abandon.AbandonController()

View 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 ''

View File

@ -39,6 +39,7 @@ class ZonesView(base_view.BaseView):
"ttl": zone['ttl'],
"serial": zone['serial'],
"status": zone['status'],
"action": zone['action'],
"version": zone['version'],
"created_at": zone['created_at'],
"updated_at": zone['updated_at'],

View File

@ -30,6 +30,7 @@ from oslo_concurrency import lockutils
from designate.i18n import _LI
from designate.i18n import _LC
from designate.i18n import _LW
from designate import context as dcontext
from designate import exceptions
from designate import network_api
@ -914,7 +915,10 @@ class Service(service.RPCService):
'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
criterion = {'parent_domain_id': domain_id}
@ -923,9 +927,12 @@ class Service(service.RPCService):
raise exceptions.DomainHasSubdomain('Please delete any subdomains '
'before deleting this domain')
domain = self._delete_domain_in_storage(context, domain)
self.pool_manager_api.delete_domain(context, domain)
if hasattr(context, 'abandon') and context.abandon:
LOG.info(_LW("Abandoning zone '%(zone)s'") % {'zone': domain.name})
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
@ -1997,6 +2004,8 @@ class Service(service.RPCService):
# used to indicate the domain has been deleted and not the deleted
# column. The deleted column is needed for unique constraints.
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)
def _update_record_status(self, context, domain_id, status, serial):

View File

@ -29,12 +29,13 @@ LOG = logging.getLogger(__name__)
class DesignateContext(context.RequestContext):
_all_tenants = False
_abandon = None
def __init__(self, auth_token=None, user=None, tenant=None, domain=None,
user_domain=None, project_domain=None, is_admin=False,
read_only=False, show_deleted=False, request_id=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
# it is a generated field based on several others.
@ -56,6 +57,7 @@ class DesignateContext(context.RequestContext):
self.service_catalog = service_catalog
self.all_tenants = all_tenants
self.abandon = abandon
if not hasattr(local.store, 'context'):
self.update_store()
@ -75,6 +77,7 @@ class DesignateContext(context.RequestContext):
'roles': self.roles,
'service_catalog': self.service_catalog,
'all_tenants': self.all_tenants,
'abandon': self.abandon,
})
return copy.deepcopy(d)
@ -128,3 +131,13 @@ class DesignateContext(context.RequestContext):
if value:
policy.check('all_tenants', self)
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

View File

@ -61,7 +61,13 @@
"status": {
"type": "string",
"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
},
"serial": {

View File

@ -285,6 +285,11 @@ class SQLAlchemy(object):
obj.deleted = obj.id.replace('-', '')
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
# be raised in this call, therefore, it is OK to pass
# in "None" as the exc_dup param.

View File

@ -243,7 +243,7 @@ class ApiV2RecordSetsTest(ApiV2TestCase):
self.assertEqual(200, response.status_int)
# 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
domain_serial = self.central_service.get_domain(

View File

@ -334,7 +334,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
def test_delete_zone(self):
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):
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,
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
def test_missing_origin(self):
fixture = self.get_zonefile_fixture(variant='noorigin')

View File

@ -261,7 +261,9 @@ Delete Zone
.. 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:**
@ -276,9 +278,9 @@ Delete Zone
.. sourcecode:: http
HTTP/1.1 204 No Content
HTTP/1.1 202 Accepted
:statuscode 204: No content
:statuscode 202: Accepted
Import Zone
-----------
@ -380,6 +382,33 @@ Export Zone
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
-------------

View File

@ -48,6 +48,7 @@
"find_domain": "rule:admin_or_owner",
"update_domain": "rule:admin_or_owner",
"delete_domain": "rule:admin_or_owner",
"abandon_domain": "rule:admin",
"count_domains": "rule:admin_or_owner",
"touch_domain": "rule:admin_or_owner",