From 544a5063f39b1b785351e55a5553481c7c65572e Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 7 Aug 2019 16:40:09 -0400 Subject: [PATCH] Update timeout implementation, placeholder code for possible websocket support --- awx/api/serializers.py | 20 +++++++++++++++---- awx/api/views/__init__.py | 6 ++++-- awx/main/access.py | 5 ++--- .../migrations/0083_v360_workflow_approval.py | 5 +++++ awx/main/models/workflow.py | 13 +++++++++++- awx/main/scheduler/task_manager.py | 19 ++++++++++++------ 6 files changed, 52 insertions(+), 16 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b79ae9ccb8..0095b3695c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -121,8 +121,8 @@ SUMMARIZABLE_FK_FIELDS = { 'job_template': DEFAULT_SUMMARY_FIELDS, 'workflow_job_template': DEFAULT_SUMMARY_FIELDS, 'workflow_job': DEFAULT_SUMMARY_FIELDS, - 'workflow_approval_template': DEFAULT_SUMMARY_FIELDS, - 'workflow_approval': DEFAULT_SUMMARY_FIELDS, + 'workflow_approval_template': DEFAULT_SUMMARY_FIELDS + ('timeout',), + 'workflow_approval': DEFAULT_SUMMARY_FIELDS + ('timeout',), 'schedule': DEFAULT_SUMMARY_FIELDS + ('next_run',), 'unified_job_template': DEFAULT_SUMMARY_FIELDS + ('unified_job_type',), 'last_job': DEFAULT_SUMMARY_FIELDS + ('finished', 'status', 'failed', 'license_error'), @@ -3413,10 +3413,16 @@ class WorkflowApprovalViewSerializer(UnifiedJobSerializer): class WorkflowApprovalSerializer(UnifiedJobSerializer): can_approve_or_deny = serializers.SerializerMethodField() + approval_expiration = serializers.SerializerMethodField() class Meta: model = WorkflowApproval - fields = (['*', '-controller_node', '-execution_node', 'can_approve_or_deny']) + fields = (['*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration']) + + def get_approval_expiration(self, obj): + if obj.status != 'pending' or obj.timeout == 0: + return None + return now() + timedelta(seconds=obj.timeout) def get_can_approve_or_deny(self, obj): request = self.context.get('request', None) @@ -3437,9 +3443,15 @@ class WorkflowApprovalSerializer(UnifiedJobSerializer): class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer): can_approve_or_deny = serializers.SerializerMethodField() + approval_expiration = serializers.SerializerMethodField() class Meta: - fields = ('*', '-execution_node', '-controller_node', 'can_approve_or_deny') + fields = ('*', '-execution_node', '-controller_node', 'can_approve_or_deny', 'approval_expiration') + + def get_approval_expiration(self, obj): + if obj.status != 'pending' or obj.timeout == 0: + return None + return now() + timedelta(seconds=obj.timeout) def get_can_approve_or_deny(self, obj): request = self.context.get('request', None) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 7e43a4a707..d53438cb48 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -4457,7 +4457,8 @@ class WorkflowApprovalApprove(RetrieveAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() - request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj) + if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj): + raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow.")) if obj.status != 'pending': return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST) obj.approve() @@ -4471,7 +4472,8 @@ class WorkflowApprovalDeny(RetrieveAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() - request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj) + if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj): + raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow.")) if obj.status != 'pending': return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST) obj.deny() diff --git a/awx/main/access.py b/awx/main/access.py index ccebf57c75..ea4e2938f7 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2379,7 +2379,7 @@ class UnifiedJobTemplateAccess(BaseAccess): Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')) | Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs( Inventory, self.user, 'read_role')) - ).exclude(polymorphic_ctype__model='workflowapprovaltemplate') # &&&&&& + ) # &&&&&& (filter out approvals from UJT endpoint here...?) def can_start(self, obj, validate_license=True): access_class = access_registry[obj.__class__] @@ -2429,7 +2429,7 @@ class UnifiedJobAccess(BaseAccess): Q(adhoccommand__inventory__id__in=inv_pk_qs) | Q(job__inventory__organization__in=org_auditor_qs) | Q(job__project__organization__in=org_auditor_qs) - ).exclude(polymorphic_ctype__model='workflowapproval') # &&&&&& + ) # &&&&&& (for filtering out approvals from UJ endpoint...?) return qs @@ -2632,7 +2632,6 @@ class ActivityStreamAccess(BaseAccess): app_set = OAuth2ApplicationAccess(self.user).filtered_queryset() token_set = OAuth2TokenAccess(self.user).filtered_queryset() -# &&&&&& Activity Stream + RBAC here?? return qs.filter( Q(ad_hoc_command__inventory__in=inventory_set) | Q(o_auth2_application__in=app_set) | diff --git a/awx/main/migrations/0083_v360_workflow_approval.py b/awx/main/migrations/0083_v360_workflow_approval.py index 9f2b079d97..8e76fa56dc 100644 --- a/awx/main/migrations/0083_v360_workflow_approval.py +++ b/awx/main/migrations/0083_v360_workflow_approval.py @@ -70,4 +70,9 @@ class Migration(migrations.Migration): name='read_role', field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['singleton:system_auditor', 'organization.auditor_role', 'execute_role', 'admin_role', 'approval_role'], related_name='+', to='main.Role'), ), + migrations.AddField( + model_name='workflowapproval', + name='timeout', + field=models.IntegerField(blank=True, default=0, help_text='The amount of time (in seconds) before the approval node expires and fails.'), + ), ] diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 4ac20e7872..8e07d79b41 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -632,7 +632,7 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): @classmethod def _get_unified_job_field_names(cls): - return ['name', 'description'] + return ['name', 'description', 'timeout'] def get_absolute_url(self, request=None): return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request) @@ -650,6 +650,11 @@ class WorkflowApproval(UnifiedJob): default=None, on_delete=models.SET_NULL, ) + timeout = models.IntegerField( + blank=True, + default=0, + help_text=_("The amount of time (in seconds) before the approval node expires and fails."), + ) @classmethod def _get_unified_job_template_class(cls): @@ -676,3 +681,9 @@ class WorkflowApproval(UnifiedJob): self.save() schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) + + # &&&&&& Possible placeholder for websocket support + # def websocket_emit_data(self): + # websocket_data = super(WorkflowApproval, self).websocket_emit_data() + # websocket_data.update(dict(project_id=self.project.id)) # ????? + # return websocket_data diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 1af7440048..fe316c22ea 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -519,20 +519,27 @@ class TaskManager(): if not found_acceptable_queue: logger.debug("{} couldn't be scheduled on graph, waiting for next cycle".format(task.log_format)) - def timeout_approval_node(self): - workflow_approvals = WorkflowApproval.objects.filter(status='pending').prefetch_related('workflow_approval_template') + def timeout_approval_node(self): # Add websocket stuff for when it transitions to "timed out" (maybe a websocket_emit_status() call) + workflow_approvals = WorkflowApproval.objects.filter(status='pending') now = tz_now() for task in workflow_approvals: - # TODO: copy the timeout to the job itself at launch time, not the template - approval_timeout_seconds = timedelta(seconds=task.workflow_approval_template.timeout) - if task.workflow_approval_template.timeout == 0: + # TODO: copy the timeout to the job itself at launch time, not the template <---- Started to implement steps to do this, but unsure... + approval_timeout_seconds = timedelta(seconds=task.timeout) + if task.timeout == 0: continue if (now - task.created) >= approval_timeout_seconds: - logger.info("The approval node {} ({}) has expired after {} seconds.".format(task.name, task.id, task.workflow_approval_template.timeout)) + logger.info("The approval node {} ({}) has expired after {} seconds.".format(task.name, task.pk, task.timeout)) task.status = 'failed' task.job_explanation = _("This approval node has timed out.") task.save(update_fields=['status', 'job_explanation']) + # &&&&&& Placeholder for websocket support + # def detect_pending_approval(self): + # workflow_approvals = WorkflowApproval.objects.filter(status='pending').prefetch_related('workflow_approval_template') + # for task in workflow_approvals: + # if task.status == 'pending': + # workflow_approvals.websocket_emit_status(task.status) + def calculate_capacity_consumed(self, tasks): self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph)