Manager support for project cascade delete
Adds manager support for project cascade delete. This operation is done in atomic operation, with just one delete request to the backend. It keeps the normal delete behavior that requires the whole subtree disabled. Otherwise, no project is deleted. A new parameter "parent_project_id" was also added to the test helper method _create_projects_hierarchy to ease the creation of sub-hierarchies. Co-Authored-By: Henrique Truta <henrique@lsd.ufcg.edu.br> Change-Id: I066494945d99a9a4d6f97568486a74c68d8d2cd7 Partially-Implements: bp project-tree-deletion
This commit is contained in:
parent
62d4b2f669
commit
c25786a50a
@ -276,14 +276,11 @@ class Manager(manager.Manager):
|
||||
action=_('cannot enable project %s since it has '
|
||||
'disabled parents') % project_id)
|
||||
|
||||
def _assert_whole_subtree_is_disabled(self, project_id):
|
||||
subtree_list = self.list_projects_in_subtree(project_id)
|
||||
for ref in subtree_list:
|
||||
if ref.get('enabled', True):
|
||||
raise exception.ForbiddenAction(
|
||||
action=_('cannot disable project %s since '
|
||||
'its subtree contains enabled '
|
||||
'projects') % project_id)
|
||||
def _check_whole_subtree_is_disabled(self, project_id, subtree_list=None):
|
||||
if not subtree_list:
|
||||
subtree_list = self.list_projects_in_subtree(project_id)
|
||||
subtree_enabled = [ref.get('enabled', True) for ref in subtree_list]
|
||||
return (not any(subtree_enabled))
|
||||
|
||||
def update_project(self, project_id, project, initiator=None):
|
||||
# Use the driver directly to prevent using old cached value.
|
||||
@ -325,8 +322,13 @@ class Manager(manager.Manager):
|
||||
# project acting as a domain to be disabled irrespective of the
|
||||
# state of its children. Disabling a project acting as domain
|
||||
# effectively disables its children.
|
||||
if not original_project.get('is_domain'):
|
||||
self._assert_whole_subtree_is_disabled(project_id)
|
||||
if (not original_project.get('is_domain') and not
|
||||
self._check_whole_subtree_is_disabled(project_id)):
|
||||
raise exception.ForbiddenAction(
|
||||
action=_('cannot disable project %(project_id)s since its '
|
||||
'subtree contains enabled projects.')
|
||||
% {'project_id': project_id})
|
||||
|
||||
self._disable_project(project_id)
|
||||
|
||||
ret = self.driver.update_project(project_id, project)
|
||||
@ -344,7 +346,27 @@ class Manager(manager.Manager):
|
||||
|
||||
return ret
|
||||
|
||||
def delete_project(self, project_id, initiator=None):
|
||||
def _pre_delete_cleanup_project(self, project_id, project, initiator=None):
|
||||
project_user_ids = (
|
||||
self.assignment_api.list_user_ids_for_project(project_id))
|
||||
for user_id in project_user_ids:
|
||||
payload = {'user_id': user_id, 'project_id': project_id}
|
||||
self._emit_invalidate_user_project_tokens_notification(payload)
|
||||
|
||||
def _post_delete_cleanup_project(self, project_id, project,
|
||||
initiator=None):
|
||||
self.assignment_api.delete_project_assignments(project_id)
|
||||
self.get_project.invalidate(self, project_id)
|
||||
self.get_project_by_name.invalidate(self, project['name'],
|
||||
project['domain_id'])
|
||||
self.credential_api.delete_credentials_for_project(project_id)
|
||||
notifications.Audit.deleted(self._PROJECT, project_id, initiator)
|
||||
# Invalidate user role assignments cache region, as it may
|
||||
# be caching role assignments where the target is
|
||||
# the specified project
|
||||
assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
|
||||
|
||||
def delete_project(self, project_id, initiator=None, cascade=False):
|
||||
# Use the driver directly to prevent using old cached value.
|
||||
project = self.driver.get_project(project_id)
|
||||
if project['is_domain'] and project['enabled']:
|
||||
@ -353,27 +375,37 @@ class Manager(manager.Manager):
|
||||
'domain. Please disable the project %s first.')
|
||||
% project.get('id'))
|
||||
|
||||
if not self.is_leaf_project(project_id):
|
||||
if not self.is_leaf_project(project_id) and not cascade:
|
||||
raise exception.ForbiddenAction(
|
||||
action=_('cannot delete the project %s since it is not '
|
||||
'a leaf in the hierarchy.') % project_id)
|
||||
'a leaf in the hierarchy. Use the cascade option '
|
||||
'if you want to delete a whole subtree.')
|
||||
% project_id)
|
||||
|
||||
project_user_ids = (
|
||||
self.assignment_api.list_user_ids_for_project(project_id))
|
||||
for user_id in project_user_ids:
|
||||
payload = {'user_id': user_id, 'project_id': project_id}
|
||||
self._emit_invalidate_user_project_tokens_notification(payload)
|
||||
ret = self.driver.delete_project(project_id)
|
||||
self.assignment_api.delete_project_assignments(project_id)
|
||||
self.get_project.invalidate(self, project_id)
|
||||
self.get_project_by_name.invalidate(self, project['name'],
|
||||
project['domain_id'])
|
||||
self.credential_api.delete_credentials_for_project(project_id)
|
||||
notifications.Audit.deleted(self._PROJECT, project_id, initiator)
|
||||
if cascade:
|
||||
# Getting reversed project's subtrees list, i.e. from the leaves
|
||||
# to the root, so we do not break parent_id FK.
|
||||
subtree_list = self.list_projects_in_subtree(project_id)
|
||||
subtree_list.reverse()
|
||||
if not self._check_whole_subtree_is_disabled(
|
||||
project_id, subtree_list=subtree_list):
|
||||
raise exception.ForbiddenAction(
|
||||
action=_('cannot delete project %(project_id)s since its '
|
||||
'subtree contains enabled projects.')
|
||||
% {'project_id': project_id})
|
||||
|
||||
# Invalidate user role assignments cache region, as it may be caching
|
||||
# role assignments where the target is the specified project
|
||||
assignment.COMPUTED_ASSIGNMENTS_REGION.invalidate()
|
||||
project_list = subtree_list + [project]
|
||||
projects_ids = [x['id'] for x in project_list]
|
||||
|
||||
for prj in project_list:
|
||||
self._pre_delete_cleanup_project(prj['id'], prj, initiator)
|
||||
ret = self.driver.delete_projects_from_ids(projects_ids)
|
||||
for prj in project_list:
|
||||
self._post_delete_cleanup_project(prj['id'], prj, initiator)
|
||||
else:
|
||||
self._pre_delete_cleanup_project(project_id, project, initiator)
|
||||
ret = self.driver.delete_project(project_id)
|
||||
self._post_delete_cleanup_project(project_id, project, initiator)
|
||||
|
||||
return ret
|
||||
|
||||
|
@ -34,3 +34,9 @@ class SqlIdentityV8(test_backend_sql.SqlIdentity):
|
||||
|
||||
def test_delete_projects_from_ids_with_no_existing_project_id(self):
|
||||
self.skipTest('Operation not supported in v8 and earlier drivers')
|
||||
|
||||
def test_delete_project_cascade(self):
|
||||
self.skipTest('Operation not supported in v8 and earlier drivers')
|
||||
|
||||
def test_delete_large_project_cascade(self):
|
||||
self.skipTest('Operation not supported in v8 and earlier drivers')
|
||||
|
@ -2524,7 +2524,8 @@ class IdentityTests(AssignmentTestHelperMixin):
|
||||
|
||||
def _create_projects_hierarchy(self, hierarchy_size=2,
|
||||
domain_id=DEFAULT_DOMAIN_ID,
|
||||
is_domain=False):
|
||||
is_domain=False,
|
||||
parent_project_id=None):
|
||||
"""Creates a project hierarchy with specified size.
|
||||
|
||||
:param hierarchy_size: the desired hierarchy size, default is 2 -
|
||||
@ -2532,12 +2533,20 @@ class IdentityTests(AssignmentTestHelperMixin):
|
||||
:param domain_id: domain where the projects hierarchy will be created.
|
||||
:param is_domain: if the hierarchy will have the is_domain flag active
|
||||
or not.
|
||||
:param parent_project_id: if the intention is to create a
|
||||
sub-hierarchy, sets the sub-hierarchy root. Defaults to creating
|
||||
a new hierarchy, i.e. a new root project.
|
||||
|
||||
:returns projects: a list of the projects in the created hierarchy.
|
||||
|
||||
"""
|
||||
project = unit.new_project_ref(domain_id=domain_id,
|
||||
is_domain=is_domain)
|
||||
if parent_project_id:
|
||||
project = unit.new_project_ref(parent_id=parent_project_id,
|
||||
domain_id=domain_id,
|
||||
is_domain=is_domain)
|
||||
else:
|
||||
project = unit.new_project_ref(domain_id=domain_id,
|
||||
is_domain=is_domain)
|
||||
project_id = project['id']
|
||||
self.resource_api.create_project(project_id, project)
|
||||
|
||||
@ -3356,6 +3365,83 @@ class IdentityTests(AssignmentTestHelperMixin):
|
||||
# no error.
|
||||
self.resource_api.driver.delete_projects_from_ids([uuid.uuid4().hex])
|
||||
|
||||
def test_delete_project_cascade(self):
|
||||
# create a hierarchy with 3 levels
|
||||
projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3)
|
||||
root_project = projects_hierarchy[0]
|
||||
project1 = projects_hierarchy[1]
|
||||
project2 = projects_hierarchy[2]
|
||||
|
||||
# Disabling all projects before attempting to delete
|
||||
for project in (project2, project1, root_project):
|
||||
project['enabled'] = False
|
||||
self.resource_api.update_project(project['id'], project)
|
||||
|
||||
self.resource_api.delete_project(root_project['id'], cascade=True)
|
||||
|
||||
for project in projects_hierarchy:
|
||||
self.assertRaises(exception.ProjectNotFound,
|
||||
self.resource_api.get_project,
|
||||
project['id'])
|
||||
|
||||
def test_delete_large_project_cascade(self):
|
||||
"""Try delete a large project with cascade true
|
||||
Tree we will create::
|
||||
|
||||
+-p1-+
|
||||
| |
|
||||
p5 p2
|
||||
| |
|
||||
p6 +-p3-+
|
||||
| |
|
||||
p7 p4
|
||||
"""
|
||||
# create a hierarchy with 4 levels
|
||||
projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=4)
|
||||
p1 = projects_hierarchy[0]
|
||||
# Add the left branch to the hierarchy (p5, p6)
|
||||
self._create_projects_hierarchy(hierarchy_size=2,
|
||||
parent_project_id=p1['id'])
|
||||
# Add p7 to the hierarchy
|
||||
p3_id = projects_hierarchy[2]['id']
|
||||
self._create_projects_hierarchy(hierarchy_size=1,
|
||||
parent_project_id=p3_id)
|
||||
# Reverse the hierarchy to disable the leaf first
|
||||
prjs_hierarchy = ([p1] + self.resource_api.list_projects_in_subtree(
|
||||
p1['id']))[::-1]
|
||||
|
||||
# Disabling all projects before attempting to delete
|
||||
for project in prjs_hierarchy:
|
||||
project['enabled'] = False
|
||||
self.resource_api.update_project(project['id'], project)
|
||||
|
||||
self.resource_api.delete_project(p1['id'], cascade=True)
|
||||
for project in prjs_hierarchy:
|
||||
self.assertRaises(exception.ProjectNotFound,
|
||||
self.resource_api.get_project,
|
||||
project['id'])
|
||||
|
||||
def test_cannot_delete_project_cascade_with_enabled_child(self):
|
||||
# create a hierarchy with 3 levels
|
||||
projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3)
|
||||
root_project = projects_hierarchy[0]
|
||||
project1 = projects_hierarchy[1]
|
||||
project2 = projects_hierarchy[2]
|
||||
|
||||
project2['enabled'] = False
|
||||
self.resource_api.update_project(project2['id'], project2)
|
||||
|
||||
# Cannot cascade delete root_project, since project1 is enabled
|
||||
self.assertRaises(exception.ForbiddenAction,
|
||||
self.resource_api.delete_project,
|
||||
root_project['id'],
|
||||
cascade=True)
|
||||
|
||||
# Ensuring no project was deleted, not even project2
|
||||
self.resource_api.get_project(root_project['id'])
|
||||
self.resource_api.get_project(project1['id'])
|
||||
self.resource_api.get_project(project2['id'])
|
||||
|
||||
def test_hierarchical_projects_crud(self):
|
||||
# create a hierarchy with just a root project (which is a leaf as well)
|
||||
projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=1)
|
||||
|
Loading…
Reference in New Issue
Block a user