Merge "Add a new API for abandoning a zone from storage"

This commit is contained in:
Jenkins 2015-02-12 19:54:58 +00:00 committed by Gerrit Code Review
commit 86dddab7e3
12 changed files with 131 additions and 19 deletions

View File

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

View File

@ -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()

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'], "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'],

View File

@ -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):

View File

@ -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

View File

@ -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": {

View File

@ -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.

View File

@ -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(

View File

@ -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')

View File

@ -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
------------- -------------

View File

@ -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",