diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 9659a35ba0..18bfce0872 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1381,6 +1381,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 f201791da7..2886e8c871 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1361,6 +1361,7 @@ class RunProjectUpdate(BaseTask): def _update_dependent_inventories(self, project_update, dependent_inventory_sources): project_request_id = '' if self.request.id is None else self.request.id scm_revision = project_update.project.scm_revision + inv_update_class = InventoryUpdate._get_task_class() for inv_src in dependent_inventory_sources: if not inv_src.update_on_project_update: continue @@ -1381,15 +1382,23 @@ class RunProjectUpdate(BaseTask): status='running', celery_task_id=str(project_request_id), source_project_update=project_update)) - inv_update_task = local_inv_update._get_task_class() try: - task_instance = inv_update_task() + task_instance = inv_update_class() # Runs in the same Celery task as project update task_instance.request.id = project_request_id task_instance.run(local_inv_update.id) 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']) @@ -1433,7 +1442,6 @@ class RunProjectUpdate(BaseTask): if instance.launch_type == 'sync': self.release_lock(instance) p = instance.project - dependent_inventory_sources = p.scm_inventory_sources.all() if instance.job_type == 'check' and status not in ('failed', 'canceled',): fd = open(self.revision_path, 'r') lines = fd.readlines() @@ -1442,11 +1450,11 @@ class RunProjectUpdate(BaseTask): else: logger.info("Could not find scm revision in check") p.playbook_files = p.playbooks - if len(dependent_inventory_sources) > 0: - p.inventory_files = p.inventories + p.inventory_files = p.inventories p.save() # Update any inventories that depend on this project + dependent_inventory_sources = p.scm_inventory_sources.filter(update_on_project_update=True) if len(dependent_inventory_sources) > 0: if status == 'successful' and instance.launch_type != 'sync': self._update_dependent_inventories(instance, dependent_inventory_sources) 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()