Allow cluster delete to detach policies & delete receivers

This patch allows the cluster delete actions to detach policies
and delete receivers for the cluster being deleted. This
simplifies deleting clusters by not having to detach or delete
all dependancies from it beforehand.

Depends-On: https://review.opendev.org/657713/

Change-Id: I9c723516a65a43533e0589bc85bd485a6387711b
This commit is contained in:
Jude Cross 2019-05-07 15:41:10 -07:00
parent 1a3e61aeab
commit e37e347771
8 changed files with 138 additions and 107 deletions

View File

@ -57,6 +57,7 @@
clustering:
min_microversion: 1.12
max_microversion: 1.12
delete_with_dependency: True
- job:
name: senlin-dsvm-tempest-py27-api

View File

@ -322,10 +322,6 @@ Deletes a cluster.
Response Codes
--------------
A cluster cannot be deleted if there are still policies attached to it.
It cannot be deleted if there are still receivers associated with it. In both
cases, a 409 error will be returned.
.. rest_status_code:: success status.yaml
- 202

View File

@ -0,0 +1,5 @@
---
features:
- |
Allow the cluster delete action to detach policies and delete receivers instead
of erroring.

View File

@ -240,18 +240,19 @@ class Action(object):
return cls._from_object(db_action)
@classmethod
def create(cls, ctx, target, action, **kwargs):
def create(cls, ctx, target, action, force=False, **kwargs):
"""Create an action object.
:param ctx: The requesting context.
:param target: The ID of the target cluster/node.
:param action: Name of the action.
:param force: Skip checking locks/conflicts
:param dict kwargs: Other keyword arguments for the action.
:return: ID of the action created.
"""
cls._check_action_lock(target, action)
cls._check_conflicting_actions(ctx, target, action)
if not force:
cls._check_action_lock(target, action)
cls._check_conflicting_actions(ctx, target, action)
params = {
'user_id': ctx.user_id,

View File

@ -30,8 +30,10 @@ from senlin.engine import scheduler
from senlin.engine import senlin_lock
from senlin.objects import action as ao
from senlin.objects import cluster as co
from senlin.objects import cluster_policy as cp_obj
from senlin.objects import dependency as dobj
from senlin.objects import node as no
from senlin.objects import receiver as receiver_obj
from senlin.policies import base as policy_mod
LOG = logging.getLogger(__name__)
@ -496,6 +498,22 @@ class ClusterAction(base.Action):
:returns: A tuple containing the result and the corresponding reason.
"""
# Detach policies before delete
policies = cp_obj.ClusterPolicy.get_all(self.context, self.entity.id)
for policy in policies:
res, reason = self.entity.detach_policy(self.context,
policy.policy_id)
if res:
self.entity.store(self.context)
else:
return self.RES_ERROR, ("Unable to detach policy {} before "
"deletion.".format(policy.id))
# Delete receivers
receivers = receiver_obj.Receiver.get_all(
self.context, filters={'cluster_id': self.entity.id})
for receiver in receivers:
receiver_obj.Receiver.delete(self.context, receiver.id)
reason = 'Deletion in progress.'
self.entity.set_status(self.context, consts.CS_DELETING, reason)
node_ids = [node.id for node in self.entity.nodes]

View File

@ -904,21 +904,6 @@ class EngineService(service.Service):
LOG.error(err)
msg.append(err)
policies = cp_obj.ClusterPolicy.get_all(ctx, cluster.id)
if len(policies) > 0:
err = _('Cluster %(id)s cannot be deleted without having all '
'policies detached.') % {'id': req.identity}
LOG.error(err)
msg.append(_("there is still policy(s) attached to it."))
receivers = receiver_obj.Receiver.get_all(
ctx, filters={'cluster_id': cluster.id})
if len(receivers) > 0:
err = _('Cluster %(id)s cannot be deleted without having all '
'receivers deleted.') % {'id': req.identity}
LOG.error(err)
msg.append(_("there is still receiver(s) associated with it."))
if msg:
raise exception.ResourceInUse(type='cluster', id=req.identity,
reason='\n'.join(msg))
@ -929,7 +914,8 @@ class EngineService(service.Service):
'status': action_mod.Action.READY,
}
action_id = action_mod.Action.create(ctx, cluster.id,
consts.CLUSTER_DELETE, **params)
consts.CLUSTER_DELETE,
force=True, **params)
dispatcher.start_action()
LOG.info("Cluster delete action queued: %s", action_id)

View File

@ -19,8 +19,10 @@ from senlin.engine import cluster as cm
from senlin.engine import dispatcher
from senlin.engine.notifications import message as msg
from senlin.objects import action as ao
from senlin.objects import cluster_policy as cpo
from senlin.objects import dependency as dobj
from senlin.objects import node as no
from senlin.objects import receiver as ro
from senlin.tests.unit.common import base
from senlin.tests.unit.common import utils
@ -555,6 +557,106 @@ class ClusterDeleteTest(base.SenlinTestCase):
action_excluded=['CLUSTER_DELETE'],
status=['SUCCEEDED', 'FAILED'])
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(ao.Action, 'delete_by_target')
def test_do_delete_with_policies(self, mock_action, mock_policies,
mock_receivers, mock_load):
mock_policy1 = mock.Mock()
mock_policy1.policy_id = 'POLICY_ID1'
mock_policy2 = mock.Mock()
mock_policy2.policy_id = 'POLICY_ID2'
mock_policies.return_value = [mock_policy1, mock_policy2]
mock_receivers.return_value = []
node1 = mock.Mock(id='NODE_1')
node2 = mock.Mock(id='NODE_2')
cluster = mock.Mock(id='FAKE_CLUSTER', nodes=[node1, node2],
DELETING='DELETING')
cluster.do_delete.return_value = True
mock_load.return_value = cluster
cluster.detach_policy = mock.Mock()
cluster.detach_policy.return_value = (True, 'OK')
action = ca.ClusterAction(cluster.id, 'CLUSTER_DELETE', self.ctx)
action.data = {}
mock_delete = self.patchobject(action, '_delete_nodes',
return_value=(action.RES_OK, 'Good'))
# do it
res_code, res_msg = action.do_delete()
self.assertEqual(action.RES_OK, res_code)
self.assertEqual('Good', res_msg)
self.assertEqual({'deletion': {'destroy_after_deletion': True}},
action.data)
cluster.set_status.assert_called_once_with(action.context, 'DELETING',
'Deletion in progress.')
mock_delete.assert_called_once_with(['NODE_1', 'NODE_2'])
cluster.do_delete.assert_called_once_with(action.context)
mock_action.assert_called_once_with(
action.context, 'FAKE_CLUSTER',
action_excluded=['CLUSTER_DELETE'],
status=['SUCCEEDED', 'FAILED'])
detach_calls = [mock.call(action.context, 'POLICY_ID1'),
mock.call(action.context, 'POLICY_ID2')]
cluster.detach_policy.assert_has_calls(detach_calls)
@mock.patch.object(ro.Receiver, 'delete')
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(ao.Action, 'delete_by_target')
def test_do_delete_with_receivers(self, mock_action, mock_policies,
mock_receivers, mock_rec_delete,
mock_load):
mock_receiver1 = mock.Mock()
mock_receiver1.id = 'RECEIVER_ID1'
mock_receiver2 = mock.Mock()
mock_receiver2.id = 'RECEIVER_ID2'
mock_policies.return_value = []
mock_receivers.return_value = [mock_receiver1, mock_receiver2]
node1 = mock.Mock(id='NODE_1')
node2 = mock.Mock(id='NODE_2')
cluster = mock.Mock(id='FAKE_CLUSTER', nodes=[node1, node2],
DELETING='DELETING')
cluster.do_delete.return_value = True
mock_load.return_value = cluster
cluster.detach_policy = mock.Mock()
cluster.detach_policy.return_value = (True, 'OK')
action = ca.ClusterAction(cluster.id, 'CLUSTER_DELETE', self.ctx)
action.data = {}
mock_delete = self.patchobject(action, '_delete_nodes',
return_value=(action.RES_OK, 'Good'))
# do it
res_code, res_msg = action.do_delete()
self.assertEqual(action.RES_OK, res_code)
self.assertEqual('Good', res_msg)
self.assertEqual({'deletion': {'destroy_after_deletion': True}},
action.data)
cluster.set_status.assert_called_once_with(action.context, 'DELETING',
'Deletion in progress.')
mock_delete.assert_called_once_with(['NODE_1', 'NODE_2'])
cluster.do_delete.assert_called_once_with(action.context)
mock_action.assert_called_once_with(
action.context, 'FAKE_CLUSTER',
action_excluded=['CLUSTER_DELETE'],
status=['SUCCEEDED', 'FAILED'])
cluster.detach_policy.assert_not_called()
rec_delete_calls = [mock.call(action.context, 'RECEIVER_ID1'),
mock.call(action.context, 'RECEIVER_ID2')]
mock_rec_delete.assert_has_calls(rec_delete_calls)
def test_do_delete_failed_delete_nodes_timeout(self, mock_load):
node = mock.Mock(id='NODE_1')
cluster = mock.Mock(id='CID', nodes=[node], ACTIVE='ACTIVE',

View File

@ -2042,16 +2042,11 @@ class ClusterTest(base.SenlinTestCase):
self.assertEqual(0, x_node_2.get_details.call_count)
@mock.patch.object(am.Action, 'create')
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(co.Cluster, 'find')
@mock.patch.object(dispatcher, 'start_action')
def test_cluster_delete(self, notify, mock_find, mock_policies,
mock_receivers, mock_action):
def test_cluster_delete(self, notify, mock_find, mock_action):
x_obj = mock.Mock(id='12345678AB', status='ACTIVE', dependents={})
mock_find.return_value = x_obj
mock_policies.return_value = []
mock_receivers.return_value = []
mock_action.return_value = 'ACTION_ID'
req = orco.ClusterDeleteRequest(identity='IDENTITY', force=False)
@ -2059,13 +2054,11 @@ class ClusterTest(base.SenlinTestCase):
self.assertEqual({'action': 'ACTION_ID'}, result)
mock_find.assert_called_once_with(self.ctx, 'IDENTITY')
mock_policies.assert_called_once_with(self.ctx, '12345678AB')
mock_receivers.assert_called_once_with(
self.ctx, filters={'cluster_id': '12345678AB'})
mock_action.assert_called_once_with(
self.ctx, '12345678AB', 'CLUSTER_DELETE',
name='cluster_delete_12345678',
cause=consts.CAUSE_RPC,
force=True,
status=am.Action.READY)
notify.assert_called_once_with()
@ -2121,77 +2114,6 @@ class ClusterTest(base.SenlinTestCase):
"The cluster 'BUSY' is in status %s." % bad_status,
six.text_type(ex.exc_info[1]))
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(co.Cluster, 'find')
def test_cluster_delete_policy_attached(self, mock_find, mock_policies):
x_obj = mock.Mock(id='12345678AB', dependents={})
mock_find.return_value = x_obj
mock_policies.return_value = [mock.Mock()]
req = orco.ClusterDeleteRequest(identity='IDENTITY',
force=False)
ex = self.assertRaises(rpc.ExpectedException,
self.eng.cluster_delete,
self.ctx, req.obj_to_primitive())
self.assertEqual(exc.ResourceInUse, ex.exc_info[0])
expected_msg = _("The cluster 'IDENTITY' cannot be deleted: "
"there is still policy(s) attached to it.")
self.assertEqual(expected_msg, six.text_type(ex.exc_info[1]))
mock_find.assert_called_once_with(self.ctx, 'IDENTITY')
mock_policies.assert_called_once_with(self.ctx, '12345678AB')
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(co.Cluster, 'find')
def test_cluster_delete_with_receiver(self, mock_find, mock_policies,
mock_receivers):
x_obj = mock.Mock(id='12345678AB', dependents={})
mock_find.return_value = x_obj
mock_policies.return_value = []
mock_receivers.return_value = [mock.Mock()]
req = orco.ClusterDeleteRequest(identity='IDENTITY',
force=False)
ex = self.assertRaises(rpc.ExpectedException,
self.eng.cluster_delete,
self.ctx, req.obj_to_primitive())
self.assertEqual(exc.ResourceInUse, ex.exc_info[0])
expected_msg = _("The cluster 'IDENTITY' cannot be deleted: "
"there is still receiver(s) associated with it.")
self.assertEqual(expected_msg, six.text_type(ex.exc_info[1]))
mock_find.assert_called_once_with(self.ctx, 'IDENTITY')
mock_policies.assert_called_once_with(self.ctx, '12345678AB')
mock_receivers.assert_called_once_with(
self.ctx, filters={'cluster_id': '12345678AB'})
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@mock.patch.object(co.Cluster, 'find')
def test_cluster_delete_mult_err(self, mock_find, mock_policies,
mock_receivers):
x_obj = mock.Mock(id='12345678AB', dependents={})
mock_find.return_value = x_obj
mock_policies.return_value = [mock.Mock()]
mock_receivers.return_value = [mock.Mock()]
req = orco.ClusterDeleteRequest(identity='IDENTITY',
force=False)
ex = self.assertRaises(rpc.ExpectedException,
self.eng.cluster_delete,
self.ctx, req.obj_to_primitive())
self.assertEqual(exc.ResourceInUse, ex.exc_info[0])
self.assertIn('there is still policy(s) attached to it.',
six.text_type(ex.exc_info[1]))
self.assertIn('there is still receiver(s) associated with it.',
six.text_type(ex.exc_info[1]))
mock_find.assert_called_once_with(self.ctx, 'IDENTITY')
mock_policies.assert_called_once_with(self.ctx, '12345678AB')
mock_receivers.assert_called_once_with(
self.ctx, filters={'cluster_id': '12345678AB'})
@mock.patch.object(am.Action, 'create')
@mock.patch.object(ro.Receiver, 'get_all')
@mock.patch.object(cpo.ClusterPolicy, 'get_all')
@ -2213,13 +2135,13 @@ class ClusterTest(base.SenlinTestCase):
self.assertEqual({'action': 'ACTION_ID'}, result)
mock_find.assert_called_with(self.ctx, 'IDENTITY')
mock_policies.assert_called_with(self.ctx, '12345678AB')
mock_receivers.assert_called_with(
self.ctx, filters={'cluster_id': '12345678AB'})
mock_policies.assert_not_called()
mock_receivers.assert_not_called()
mock_action.assert_called_with(
self.ctx, '12345678AB', 'CLUSTER_DELETE',
name='cluster_delete_12345678',
cause=consts.CAUSE_RPC,
force=True,
status=am.Action.READY)
notify.assert_called_with()