From 6be2d84adbe9965818499647b3546b5e83be3a76 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 4 Sep 2019 12:06:37 -0400 Subject: [PATCH] Add endpoints for approval node notifications ...and also add a migration file. --- awx/api/serializers.py | 8 ++- awx/api/urls/organization.py | 3 + awx/api/urls/workflow_approval_template.py | 3 + awx/api/urls/workflow_job_template.py | 3 + awx/api/views/__init__.py | 18 ++++++ awx/api/views/organization.py | 5 ++ .../0088_v360_approval_node_notifications.py | 28 +++++++++ awx/main/models/base.py | 7 +++ awx/main/models/notifications.py | 5 +- awx/main/models/unified_jobs.py | 4 ++ awx/main/models/workflow.py | 58 ++++++++++++++++++- awx/main/scheduler/task_manager.py | 6 ++ 12 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 awx/main/migrations/0088_v360_approval_node_notifications.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c7a322489b..84e1ed4051 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1261,6 +1261,7 @@ class OrganizationSerializer(BaseSerializer): notification_templates_started = self.reverse('api:organization_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:organization_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:organization_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}), @@ -3335,6 +3336,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo notification_templates_started = self.reverse('api:workflow_job_template_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:workflow_job_template_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:workflow_job_template_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}), survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}), @@ -3490,7 +3492,11 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): if 'last_job' in res: del res['last_job'] - res.update(dict(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}),)) + # Placeholder... + res.update(dict( + jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), + approval_notifications = self.reverse('api:workflow_approval_template_notification_list', kwargs={'pk': obj.pk}), + )) return res diff --git a/awx/api/urls/organization.py b/awx/api/urls/organization.py index 952209423e..1b0997b05c 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -18,6 +18,7 @@ from awx.api.views import ( OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationObjectRolesList, OrganizationAccessList, @@ -43,6 +44,8 @@ urls = [ name='organization_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', OrganizationNotificationTemplatesSuccessList.as_view(), name='organization_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(), + name='organization_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'), url(r'^(?P[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'), url(r'^(?P[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'), diff --git a/awx/api/urls/workflow_approval_template.py b/awx/api/urls/workflow_approval_template.py index 8a22ee83b3..af25a0d1e1 100644 --- a/awx/api/urls/workflow_approval_template.py +++ b/awx/api/urls/workflow_approval_template.py @@ -6,12 +6,15 @@ from django.conf.urls import url from awx.api.views import ( WorkflowApprovalTemplateDetail, WorkflowApprovalTemplateJobsList, + WorkflowApprovalNotificationTemplatesList, ) urls = [ url(r'^(?P[0-9]+)/$', WorkflowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'), url(r'^(?P[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'), + url(r'^(?P[0-9]+)/approval_notifications/$', WorkflowApprovalNotificationTemplatesList.as_view(), + name='workflow_approval_template_notification_list'), ] __all__ = ['urls'] diff --git a/awx/api/urls/workflow_job_template.py b/awx/api/urls/workflow_job_template.py index 0a33a8eaaa..349dad1aa5 100644 --- a/awx/api/urls/workflow_job_template.py +++ b/awx/api/urls/workflow_job_template.py @@ -16,6 +16,7 @@ from awx.api.views import ( WorkflowJobTemplateNotificationTemplatesErrorList, WorkflowJobTemplateNotificationTemplatesStartedList, WorkflowJobTemplateNotificationTemplatesSuccessList, + WorkflowJobTemplateNotificationTemplatesApprovalList, WorkflowJobTemplateAccessList, WorkflowJobTemplateObjectRolesList, WorkflowJobTemplateLabelList, @@ -38,6 +39,8 @@ urls = [ name='workflow_job_template_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(), name='workflow_job_template_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(), + name='workflow_job_template_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'), url(r'^(?P[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'), url(r'^(?P[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index d77ec92b91..8719ac7be4 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -119,6 +119,7 @@ from awx.api.views.organization import ( # noqa OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationAccessList, OrganizationObjectRolesList, @@ -3288,6 +3289,11 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNot relationship = 'notification_templates_success' +class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList): + + relationship = 'approval_notifications' + + class WorkflowJobTemplateAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's @@ -4458,6 +4464,18 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView): parent_key = 'workflow_approval_template' +class WorkflowApprovalTemplateNotificationTemplatesList(SubListCreateAttachDetachAPIView): + + model = models.NotificationTemplate + serializer_class = serializers.NotificationTemplateSerializer + parent_model = models.WorkflowApprovalTemplate + + +class WorkflowApprovalNotificationTemplatesList(WorkflowApprovalTemplateNotificationTemplatesList): + + relationship = 'approval_notifications' + + class WorkflowApprovalList(ListCreateAPIView): model = models.WorkflowApproval diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 6213e14f63..2f3fed49ac 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -195,6 +195,11 @@ class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTempl relationship = 'notification_templates_success' +class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList): + + relationship = 'approval_notifications' + + class OrganizationInstanceGroupsList(SubListAttachDetachAPIView): model = InstanceGroup diff --git a/awx/main/migrations/0088_v360_approval_node_notifications.py b/awx/main/migrations/0088_v360_approval_node_notifications.py new file mode 100644 index 0000000000..bb11b93b0d --- /dev/null +++ b/awx/main/migrations/0088_v360_approval_node_notifications.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.4 on 2019-09-03 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0087_v360_update_credential_injector_help_text'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='approval_notifications', + field=models.ManyToManyField(blank=True, related_name='organization_approval_notifications', to='main.NotificationTemplate'), + ), + migrations.AddField( + model_name='unifiedjobtemplate', + name='approval_notifications', + field=models.ManyToManyField(blank=True, related_name='unifiedjobtemplate_approval_notifications', to='main.NotificationTemplate'), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='do_not_run', + field=models.BooleanField(default=False, help_text='Indicates that a job will not be created when True. Workflow runtime semantics will mark this True if the node is in a path that will decidedly not be ran. A value of False means the node may not run.'), + ), + ] diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 70fa92cf0a..97aa94f1d8 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -392,6 +392,13 @@ class NotificationFieldsModel(BaseModel): related_name='%(class)s_notification_templates_for_started' ) + # Placeholder, unsure if this is required... + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) + def prevent_search(relation): """ diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index ef428bcdfa..588e97db3b 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -463,8 +463,9 @@ class JobNotificationMixin(object): def send_notification_templates(self, status): from awx.main.tasks import send_notifications # avoid circular import - if status not in ['running', 'succeeded', 'failed']: - raise ValueError(_("status must be either running, succeeded or failed")) + # Placeholder... Adding "pending" status here for approvals. + if status not in ['pending', 'running', 'succeeded', 'failed']: + raise ValueError(_("status must be either pending, running, succeeded or failed")) try: notification_templates = self.get_notification_templates() except Exception: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index b8dd1d1cf8..d4fe22806c 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -1217,6 +1217,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique status=self.status, traceback=self.result_traceback) + # Placeholder... + # def approval_notification_data(self): + # for approval in WorkflowApproval.objects.filter(workflow_approval_template=instance, status='pending'): + def pre_start(self, **kwargs): if not self.can_start: self.job_explanation = u'%s is not in a startable state: %s, expecting one of %s' % (self._meta.verbose_name, self.status, str(('new', 'waiting'))) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b2312ab63d..6d8bf14019 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -196,7 +196,7 @@ class WorkflowJobNode(WorkflowNodeBase): ) do_not_run = models.BooleanField( default=False, - help_text=_("Indidcates that a job will not be created when True. Workflow runtime " + help_text=_("Indicates that a job will not be created when True. Workflow runtime " "semantics will mark this True if the node is in a path that will " "decidedly not be ran. A value of False means the node may not run."), ) @@ -441,9 +441,12 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) success_notification_templates = list(base_notification_templates .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + approval_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_approvals__in=[self])) return dict(error=list(error_notification_templates), started=list(started_notification_templates), - success=list(success_notification_templates)) + success=list(success_notification_templates), + approvals=list(approval_notification_templates)) def create_unified_job(self, **kwargs): workflow_job = super(WorkflowJobTemplate, self).create_unified_job(**kwargs) @@ -644,12 +647,46 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): def get_absolute_url(self, request=None): return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request) + @property + def notification_templates(self): + base_notification_templates = NotificationTemplate.objects.all() + error_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self])) + started_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) + success_notification_templates = list(base_notification_templates + .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + return dict(error=list(error_notification_templates), + started=list(started_notification_templates), + success=list(success_notification_templates)) + + # base_notification_templates = NotificationTemplate.objects.all() + # approval_notification_templates = list(base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_errors__in=[self]), + # base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_started__in=[self]), + # base_notification_templates + # .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + # return dict(approval=list(approval_notification_templates)) +# Placeholder... Approval nodes don't have orgs! + # if self.project is not None and self.project.organization is None: + # error_notification_templates = set(error_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_errors=self.project.organization))) + # started_notification_templates = set(started_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_started=self.project.organization))) + # success_notification_templates = set(success_notification_templates + list(base_notification_templates.filter( + # organization_notification_templates_for_success=self.project.organization))) + # return dict(error=list(error_notification_templates), + # approval_notification_templates=list(needs_approval_notification_templates), + # success=list(success_notification_templates)) + + @property def workflow_job_template(self): return self.workflowjobtemplatenodes.first().workflow_job_template -class WorkflowApproval(UnifiedJob): +class WorkflowApproval(UnifiedJob, JobNotificationMixin): class Meta: app_label = 'main' @@ -700,6 +737,21 @@ class WorkflowApproval(UnifiedJob): schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) + # Placeholder... + def approval_notification_data(self): + result = super(WorkflowApproval, self).approval_notification_data() + str_arr = ['Approval summary:', ''] + for node in self.workflow_job_nodes.all().select_related('job'): + if node.job is None: + node_job_description = 'no job.' + else: + node_job_description = ('job #{0}, "{1}", which finished with status {2}.' + .format(node.job.id, node.job.name, node.job.status)) + str_arr.append("- node #{0} spawns {1}".format(node.id, node_job_description)) + result['body'] = '\n'.join(str_arr) + return result + + @property def workflow_job_template(self): try: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 3d89d6aecc..fbd2edd1ea 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -239,6 +239,12 @@ class TaskManager(): task.send_notification_templates('running') logger.debug('Transitioning %s to running status.', task.log_format) schedule_task_manager() + # Placeholder... + elif type(task) is WorkflowApproval: + task.status = 'pending' + task.send_notification_templates('pending') + logger.debug('Transitioning %s to pending status.', task.log_format) + schedule_task_manager() elif not task.supports_isolation() and rampart_group.controller_id: # non-Ansible jobs on isolated instances run on controller task.instance_group = rampart_group.controller