diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 660893ebf1..1240f58909 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -378,11 +378,13 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin): @transaction.atomic def schedule_deletion(self, user_id=None): from awx.main.tasks import delete_inventory + from awx.main.signals import activity_stream_delete if self.pending_deletion is True: raise RuntimeError("Inventory is already pending deletion.") self.pending_deletion = True self.save(update_fields=['pending_deletion']) self.jobtemplates.clear() + activity_stream_delete(Inventory, self, inventory_delete_flag=True) self.websocket_emit_status('pending_deletion') delete_inventory.delay(self.pk, user_id) diff --git a/awx/main/signals.py b/awx/main/signals.py index b9d669c9c2..35501c6cf9 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -434,6 +434,12 @@ def activity_stream_delete(sender, instance, **kwargs): # Skip recording any inventory source directly associated with a group. if isinstance(instance, InventorySource) and instance.deprecated_group: return + # Inventory delete happens in the task system rather than request-response-cycle. + # If we trigger this handler there we may fall into db-integrity-related race conditions. + # So we add flag verification to prevent normal signal handling. This funciton will be + # explicitly called with flag on in Inventory.schedule_deletion. + if isinstance(instance, Inventory) and not kwargs.get('inventory_delete_flag', False): + return changes = model_to_dict(instance) object1 = camelcase_to_underscore(instance.__class__.__name__) activity_entry = ActivityStream( diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index de2a3f186e..9a66a53ab1 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError from awx.api.versioning import reverse -from awx.main.models import InventorySource, Inventory +from awx.main.models import InventorySource, Inventory, ActivityStream import json @@ -23,10 +23,10 @@ def scm_inventory(inventory, project): def factory_scm_inventory(inventory, project): def fn(**kwargs): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): - return inventory.inventory_sources.create(source_project=project, + return inventory.inventory_sources.create(source_project=project, overwrite_vars=True, - source='scm', - scm_last_revision=project.scm_revision, + source='scm', + scm_last_revision=project.scm_revision, **kwargs) return fn @@ -83,6 +83,7 @@ def test_async_inventory_deletion(delete, get, inventory, alice): inventory.admin_role.members.add(alice) resp = delete(reverse('api:inventory_detail', kwargs={'pk': inventory.id}), alice) assert resp.status_code == 202 + assert ActivityStream.objects.filter(operation='delete').exists() resp = get(reverse('api:inventory_detail', kwargs={'pk': inventory.id}), alice) assert resp.status_code == 200 @@ -389,7 +390,7 @@ class TestControlledBySCM: def test_adding_inv_src_ok(self, post, scm_inventory, admin_user): post(reverse('api:inventory_inventory_sources_list', kwargs={'version': 'v2', 'pk': scm_inventory.id}), - {'name': 'new inv src', 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True}, + {'name': 'new inv src', 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True}, admin_user, expect=201) def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user):