diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cf6fa391e9..c4436424f5 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2221,6 +2221,15 @@ class InventorySourceUpdateSerializer(InventorySourceSerializer): class Meta: fields = ('can_update',) + def validate(self, attrs): + project = self.instance.source_project + if project: + failed_reason = project.get_reason_if_failed() + if failed_reason: + raise serializers.ValidationError(failed_reason) + + return super(InventorySourceUpdateSerializer, self).validate(attrs) + class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSerializer): @@ -4272,17 +4281,10 @@ class JobLaunchSerializer(BaseSerializer): # Basic validation - cannot run a playbook without a playbook if not template.project: errors['project'] = _("A project is required to run a job.") - elif template.project.status in ('error', 'failed'): - errors['playbook'] = _("Missing a revision to run due to failed project update.") - - latest_update = template.project.project_updates.last() - if latest_update is not None and latest_update.failed: - failed_validation_tasks = latest_update.project_update_events.filter( - event='runner_on_failed', - play="Perform project signature/checksum verification", - ) - if failed_validation_tasks: - errors['playbook'] = _("Last project update failed due to signature validation failure.") + else: + failure_reason = template.project.get_reason_if_failed() + if failure_reason: + errors['playbook'] = failure_reason # cannot run a playbook without an inventory if template.inventory and template.inventory.pending_deletion is True: diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 14fae507d3..90e52ed883 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -2221,6 +2221,8 @@ class InventorySourceUpdateView(RetrieveAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() + serializer = self.get_serializer(instance=obj, data=request.data) + serializer.is_valid(raise_exception=True) if obj.can_update: update = obj.update() if not update: diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 5af858fa8d..6577d24c40 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -471,6 +471,29 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn def get_absolute_url(self, request=None): return reverse('api:project_detail', kwargs={'pk': self.pk}, request=request) + def get_reason_if_failed(self): + """ + If the project is in a failed or errored state, return a human-readable + error message explaining why. Otherwise return None. + + This is used during validation in the serializer and also by + RunProjectUpdate/RunInventoryUpdate. + """ + + if self.status not in ('error', 'failed'): + return None + + latest_update = self.project_updates.last() + if latest_update is not None and latest_update.failed: + failed_validation_tasks = latest_update.project_update_events.filter( + event='runner_on_failed', + play="Perform project signature/checksum verification", + ) + if failed_validation_tasks: + return _("Last project update failed due to signature validation failure.") + + return _("Missing a revision to run due to failed project update.") + ''' RelatedJobsMixin ''' diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 3295adcc9c..3557c4110c 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -767,6 +767,10 @@ class SourceControlMixin(BaseTask): try: original_branch = None + failed_reason = project.get_reason_if_failed() + if failed_reason: + self.update_model(self.instance.pk, status='failed', job_explanation=failed_reason) + raise RuntimeError(failed_reason) project_path = project.get_project_path(check_if_exists=False) if project.scm_type == 'git' and (scm_branch and scm_branch != project.scm_branch): if os.path.exists(project_path): @@ -1056,10 +1060,6 @@ class RunJob(SourceControlMixin, BaseTask): error = _('Job could not start because no Execution Environment could be found.') self.update_model(job.pk, status='error', job_explanation=error) raise RuntimeError(error) - elif job.project.status in ('error', 'failed'): - msg = _('The project revision for this job template is unknown due to a failed update.') - job = self.update_model(job.pk, status='failed', job_explanation=msg) - raise RuntimeError(msg) if job.inventory.kind == 'smart': # cache smart inventory memberships so that the host_filter query is not