diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 3cd94c3446..00d873e658 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1393,6 +1393,8 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin): res = super(InventoryUpdate, self).cancel(job_explanation=job_explanation) if res: map(lambda x: x.cancel(job_explanation=self._build_job_explanation()), self.get_dependent_jobs()) + if self.launch_type != 'scm' and self.source_project_update: + self.source_project_update.cancel(job_explanation=job_explanation) return res diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 05125a8f8e..899141dca2 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -501,6 +501,13 @@ class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin): update_fields.append('scm_delete_on_next_update') parent_instance.save(update_fields=update_fields) + def cancel(self, job_explanation=None): + res = super(ProjectUpdate, self).cancel(job_explanation=job_explanation) + if res and self.launch_type != 'sync': + for inv_src in self.scm_inventory_updates.filter(status='running'): + inv_src.cancel(job_explanation='Source project update `{}` was canceled.'.format(self.name)) + return res + ''' JobNotificationMixin ''' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 08a0b46bdc..14bfade27b 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1391,6 +1391,15 @@ class RunProjectUpdate(BaseTask): except Exception as e: # A failed file update does not block other actions logger.error('Encountered error updating project dependent inventory: {}'.format(e)) + + # Stop all dependent inventory updates if project update was canceled + project_update.refresh_from_db() + if project_update.cancel_flag: + break + # Don't update inventory scm_revision if update was canceled + local_inv_update.refresh_from_db() + if local_inv_update.cancel_flag: + continue inv_src.scm_last_revision = scm_revision inv_src.save(update_fields=['scm_last_revision']) diff --git a/awx/main/tests/functional/test_tasks.py b/awx/main/tests/functional/test_tasks.py index ee80e85a7a..c0441536b1 100644 --- a/awx/main/tests/functional/test_tasks.py +++ b/awx/main/tests/functional/test_tasks.py @@ -3,7 +3,7 @@ import mock import os from awx.main.tasks import RunProjectUpdate, RunInventoryUpdate -from awx.main.models import ProjectUpdate, InventoryUpdate +from awx.main.models import ProjectUpdate, InventoryUpdate, InventorySource @pytest.fixture @@ -43,3 +43,32 @@ class TestDependentInventoryUpdate: inv_update = InventoryUpdate.objects.first() iu_run_mock.assert_called_once_with(inv_update.id) assert inv_update.source_project_update_id == proj_update.pk + + def test_dependent_inventory_project_cancel(self, project, inventory): + ''' + Test that dependent inventory updates exhibit good behavior on cancel + of the source project update + ''' + task = RunProjectUpdate() + proj_update = ProjectUpdate.objects.create(project=project) + + kwargs = dict( + source_project=project, + source='scm', + source_path='inventory_file', + update_on_project_update=True, + inventory=inventory + ) + + is1 = InventorySource.objects.create(name="test-scm-inv", **kwargs) + is2 = InventorySource.objects.create(name="test-scm-inv2", **kwargs) + + def user_cancels_project(pk): + ProjectUpdate.objects.all().update(cancel_flag=True) + + with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock: + iu_run_mock.side_effect = user_cancels_project + task._update_dependent_inventories(proj_update, [is1, is2]) + # Verify that it bails after 1st update, detecting a cancel + assert is2.inventory_updates.count() == 0 + iu_run_mock.assert_called_once()