From 04d6fb1f95150c8c3fc7cea83bf8571a8fb45362 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 9 May 2017 09:42:53 -0400 Subject: [PATCH 1/2] SCM inventory cancel propagation For manually initiated inventory updates, also cancel the source project of "sync" type, like jobs do For automatic inventory updates spawned from source project update, of launch type "scm", handle contigency cases --- awx/main/models/inventory.py | 2 ++ awx/main/models/projects.py | 7 ++++++ awx/main/tasks.py | 9 +++++++ awx/main/tests/functional/test_tasks.py | 31 ++++++++++++++++++++++++- 4 files changed, 48 insertions(+), 1 deletion(-) 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() From fc590535be178eef6046265ed6eb44a773d7724c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 10 May 2017 10:29:12 -0400 Subject: [PATCH 2/2] update inventory file list on every project update --- awx/main/tasks.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 14bfade27b..da91fd75b4 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1362,6 +1362,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 @@ -1382,9 +1383,8 @@ 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) @@ -1443,7 +1443,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() @@ -1452,11 +1451,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)