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:
parent
1a3e61aeab
commit
e37e347771
|
@ -57,6 +57,7 @@
|
|||
clustering:
|
||||
min_microversion: 1.12
|
||||
max_microversion: 1.12
|
||||
delete_with_dependency: True
|
||||
|
||||
- job:
|
||||
name: senlin-dsvm-tempest-py27-api
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Allow the cluster delete action to detach policies and delete receivers instead
|
||||
of erroring.
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue