Add API endpoints for workflow approvals

This commit is contained in:
beeankha
2019-07-03 14:46:59 -04:00
committed by Ryan Petrello
parent 72a65f74fd
commit 9024a514a6
11 changed files with 304 additions and 17 deletions

View File

@@ -50,16 +50,16 @@ from awx.main.constants import (
CENSOR_VALUE, CENSOR_VALUE,
) )
from awx.main.models import ( from awx.main.models import (
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource,
CredentialInputSource, CredentialType, CustomInventoryScript, CredentialType, CustomInventoryScript, Group, Host, Instance,
Group, Host, Instance, InstanceGroup, Inventory, InventorySource, InstanceGroup, Inventory, InventorySource, InventoryUpdate,
InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary, InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig,
JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification, JobNotificationMixin, JobTemplate, Label, Notification, NotificationTemplate,
NotificationTemplate, OAuth2AccessToken, OAuth2Application, Organization, OAuth2AccessToken, OAuth2Application, Organization, Project,
Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
StdoutMaxBytesExceeded, SystemJob, SystemJobEvent, SystemJobTemplate, SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, UnifiedJobTemplate, WorkflowApproval, WorkflowApprovalTemplate, WorkflowJob,
WorkflowJobTemplate, WorkflowJobTemplateNode WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
) )
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
from awx.main.models.rbac import ( from awx.main.models.rbac import (
@@ -681,6 +681,8 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
serializer_class = SystemJobTemplateSerializer serializer_class = SystemJobTemplateSerializer
elif isinstance(obj, WorkflowJobTemplate): elif isinstance(obj, WorkflowJobTemplate):
serializer_class = WorkflowJobTemplateSerializer serializer_class = WorkflowJobTemplateSerializer
elif isinstance(obj, WorkflowApprovalTemplate):
serializer_class = WorkflowApprovalTemplateSerializer
return serializer_class return serializer_class
def to_representation(self, obj): def to_representation(self, obj):
@@ -782,6 +784,8 @@ class UnifiedJobSerializer(BaseSerializer):
serializer_class = SystemJobSerializer serializer_class = SystemJobSerializer
elif isinstance(obj, WorkflowJob): elif isinstance(obj, WorkflowJob):
serializer_class = WorkflowJobSerializer serializer_class = WorkflowJobSerializer
elif isinstance(obj, WorkflowApproval):
serializer_class = WorkflowApprovalSerializer
return serializer_class return serializer_class
def to_representation(self, obj): def to_representation(self, obj):
@@ -838,6 +842,8 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
serializer_class = SystemJobListSerializer serializer_class = SystemJobListSerializer
elif isinstance(obj, WorkflowJob): elif isinstance(obj, WorkflowJob):
serializer_class = WorkflowJobListSerializer serializer_class = WorkflowJobListSerializer
elif isinstance(obj, WorkflowApproval):
serializer_class = WorkflowApprovalListSerializer
return serializer_class return serializer_class
def to_representation(self, obj): def to_representation(self, obj):
@@ -3395,6 +3401,51 @@ class WorkflowJobCancelSerializer(WorkflowJobSerializer):
fields = ('can_cancel',) fields = ('can_cancel',)
class WorkflowApprovalSerializer(UnifiedJobSerializer):
class Meta:
model = WorkflowApproval
fields = ('*', 'result_stdout', '-controller_node', '-execution_node',)
def get_related(self, obj):
res = super(WorkflowApprovalSerializer, self).get_related(obj)
if obj.workflow_approval_template:
res['workflow_approval_template'] = self.reverse('api:workflow_approval_template_detail',
kwargs={'pk': obj.workflow_approval_template.pk})
res['notifications'] = self.reverse('api:workflow_approval_notifications_list', kwargs={'pk': obj.pk})
return res
def get_result_stdout(self, obj):
return obj.result_stdout
class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer):
class Meta:
fields = ('*', '-execution_node', '-controller_node',)
class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer):
class Meta:
model = WorkflowApprovalTemplate
fields = ('*',)
def get_related(self, obj):
res = super(WorkflowApprovalTemplateSerializer, self).get_related(obj)
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}),
notification_templates_started = self.reverse('api:workflow_approval_template_notification_templates_started_list', kwargs={'pk': obj.pk}),
notification_templates_success = self.reverse('api:workflow_approval_template_notification_templates_success_list', kwargs={'pk': obj.pk}),
notification_templates_error = self.reverse('api:workflow_approval_template_notification_templates_error_list', kwargs={'pk': obj.pk}),
))
return res
class LaunchConfigurationBaseSerializer(BaseSerializer): class LaunchConfigurationBaseSerializer(BaseSerializer):
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None) scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
job_type = serializers.ChoiceField(allow_blank=True, allow_null=True, required=False, default=None, job_type = serializers.ChoiceField(allow_blank=True, allow_null=True, required=False, default=None,

View File

@@ -71,6 +71,8 @@ from .instance import urls as instance_urls
from .instance_group import urls as instance_group_urls from .instance_group import urls as instance_group_urls
from .oauth2 import urls as oauth2_urls from .oauth2 import urls as oauth2_urls
from .oauth2_root import urls as oauth2_root_urls from .oauth2_root import urls as oauth2_root_urls
from .workflow_approval_template import urls as workflow_approval_template_urls
from .workflow_approval import urls as workflow_approval_urls
v2_urls = [ v2_urls = [
@@ -131,8 +133,11 @@ v2_urls = [
url(r'^unified_job_templates/$', UnifiedJobTemplateList.as_view(), name='unified_job_template_list'), url(r'^unified_job_templates/$', UnifiedJobTemplateList.as_view(), name='unified_job_template_list'),
url(r'^unified_jobs/$', UnifiedJobList.as_view(), name='unified_job_list'), url(r'^unified_jobs/$', UnifiedJobList.as_view(), name='unified_job_list'),
url(r'^activity_stream/', include(activity_stream_urls)), url(r'^activity_stream/', include(activity_stream_urls)),
url(r'^workflow_approval_templates/', include(workflow_approval_template_urls)),
url(r'^workflow_approval/', include(workflow_approval_urls)),
] ]
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
url(r'^$', ApiRootView.as_view(), name='api_root_view'), url(r'^$', ApiRootView.as_view(), name='api_root_view'),

View File

@@ -0,0 +1,21 @@
# Copyright (c) 2017 Ansible, Inc.
# All Rights Reserved.
from django.conf.urls import url
from awx.api.views import (
WorkflowApprovalList,
WorkflowApprovalDetail,
WorkflowApprovalNotificationsList,
)
urls = [
url(r'^$', WorkflowApprovalList.as_view(), name='workflow_approval_list'),
url(r'^(?P<pk>[0-9]+)/$', WorkflowApprovalDetail.as_view(), name='workflow_approval_detail'),
url(r'^(?P<pk>[0-9]+)/approve/$', WorkflowApprovalDetail.as_view(), name='approved_workflow'),
url(r'^(?P<pk>[0-9]+)/reject/$', WorkflowApprovalDetail.as_view(), name='rejected_workflow'),
url(r'^(?P<pk>[0-9]+)/notifications/$', WorkflowApprovalNotificationsList.as_view(), name='workflow_approval_notifications_list'),
]
__all__ = ['urls']

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2017 Ansible, Inc.
# All Rights Reserved.
from django.conf.urls import url
from awx.api.views import (
WorfklowApprovalTemplateList,
WorfklowApprovalTemplateDetail,
WorkflowApprovalTemplateJobsList,
WorkflowApprovalTemplateNotificationTemplatesErrorList,
WorkflowApprovalTemplateNotificationTemplatesStartedList,
WorkflowApprovalTemplateNotificationTemplatesSuccessList,
)
urls = [
url(r'^$', WorfklowApprovalTemplateList.as_view(), name='workflow_approval_template_list'),
url(r'^(?P<pk>[0-9]+)/$', WorfklowApprovalTemplateDetail.as_view(), name='workflow_approval_template_detail'),
url(r'^(?P<pk>[0-9]+)/approvals/$', WorkflowApprovalTemplateJobsList.as_view(), name='workflow_approval_template_jobs_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_started/$', WorkflowApprovalTemplateNotificationTemplatesStartedList.as_view(),
name='workflow_approval_template_notification_templates_started_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_error/$', WorkflowApprovalTemplateNotificationTemplatesErrorList.as_view(),
name='workflow_approval_template_notification_templates_error_list'),
url(r'^(?P<pk>[0-9]+)/notification_templates_success/$', WorkflowApprovalTemplateNotificationTemplatesSuccessList.as_view(),
name='workflow_approval_template_notification_templates_success_list'),
]
__all__ = ['urls']

View File

@@ -4405,3 +4405,97 @@ for attr, value in list(locals().items()):
name = camelcase_to_underscore(attr) name = camelcase_to_underscore(attr)
view = value.as_view() view = value.as_view()
setattr(this_module, name, view) setattr(this_module, name, view)
class WorfklowApprovalTemplateList(ListAPIView):
model = models.WorkflowApprovalTemplate
serializer_class = serializers.WorkflowApprovalTemplateSerializer
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
raise PermissionDenied(_("Superuser privileges needed."))
return super(WorfklowApprovalTemplateList, self).get(request, *args, **kwargs)
class WorfklowApprovalTemplateDetail(RetrieveAPIView):
model = models.WorkflowApprovalTemplate
serializer_class = serializers.WorkflowApprovalTemplateSerializer
class WorkflowApprovalTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer
parent_model = models.WorkflowApprovalTemplate
class WorkflowApprovalTemplateNotificationTemplatesStartedList(WorkflowApprovalTemplateNotificationTemplatesAnyList):
relationship = 'notification_templates_started'
class WorkflowApprovalTemplateNotificationTemplatesErrorList(WorkflowApprovalTemplateNotificationTemplatesAnyList):
relationship = 'notification_templates_error'
class WorkflowApprovalTemplateNotificationTemplatesSuccessList(WorkflowApprovalTemplateNotificationTemplatesAnyList):
relationship = 'notification_templates_success'
class WorkflowApprovalTemplateLaunch(GenericAPIView):
model = models.WorkflowApprovalTemplate
obj_permission_type = 'start'
serializer_class = serializers.EmptySerializer
def get(self, request, *args, **kwargs):
return Response({})
def post(self, request, *args, **kwargs):
obj = self.get_object()
new_job = obj.create_unified_job(extra_vars=request.data.get('extra_vars', {}))
new_job.signal_start()
data = OrderedDict()
data['workflow_approval'] = new_job.id
data.update(serializers.WorkflowApprovalSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
headers = {'Location': new_job.get_absolute_url(request)}
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class WorkflowApprovalTemplateJobsList(SubListAPIView):
model = models.WorkflowApproval
serializer_class = serializers.WorkflowApprovalListSerializer
parent_model = models.WorkflowApprovalTemplate
relationship = 'approvals'
parent_key = 'workflow_approval_template'
class WorkflowApprovalList(ListCreateAPIView):
model = models.WorkflowApproval
serializer_class = serializers.WorkflowApprovalListSerializer
def get(self, request, *args, **kwargs):
if not request.user.is_superuser and not request.user.is_system_auditor:
raise PermissionDenied(_("Superuser privileges needed."))
return super(WorkflowApprovalList, self).get(request, *args, **kwargs)
class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
model = models.WorkflowApproval
serializer_class = serializers.WorkflowApprovalSerializer
class WorkflowApprovalNotificationsList(SubListAPIView):
model = models.Notification
serializer_class = serializers.NotificationSerializer
parent_model = models.WorkflowApproval
relationship = 'notifications'
search_fields = ('subject', 'notification_type', 'body',)

View File

@@ -124,6 +124,8 @@ class ApiVersionRootView(APIView):
data['activity_stream'] = reverse('api:activity_stream_list', request=request) data['activity_stream'] = reverse('api:activity_stream_list', request=request)
data['workflow_job_templates'] = reverse('api:workflow_job_template_list', request=request) data['workflow_job_templates'] = reverse('api:workflow_job_template_list', request=request)
data['workflow_jobs'] = reverse('api:workflow_job_list', request=request) data['workflow_jobs'] = reverse('api:workflow_job_list', request=request)
data['workflow_approval_templates'] = reverse('api:workflow_approval_template_list', request=request)
data['workflow_approval'] = reverse('api:workflow_approval_list', request=request)
data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request) data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request)
data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request) data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request)
return Response(data) return Response(data)

View File

@@ -37,6 +37,7 @@ from awx.main.models import (
ProjectUpdateEvent, Role, Schedule, SystemJob, SystemJobEvent, ProjectUpdateEvent, Role, Schedule, SystemJob, SystemJobEvent,
SystemJobTemplate, Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, SystemJobTemplate, Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob,
WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode,
WorkflowApproval, WorkflowApprovalTemplate,
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
) )
from awx.main.models.mixins import ResourceMixin from awx.main.models.mixins import ResourceMixin
@@ -2769,5 +2770,50 @@ class RoleAccess(BaseAccess):
return False return False
class WorkflowApprovalAccess(BaseAccess):
'''
I can approve workflows when:
- I'm authenticated
I can create when:
- I'm a superuser:
'''
model = WorkflowApproval
prefetch_related = ('created_by', 'modified_by',)
def can_read(self, obj):
return True
def can_use(self, obj):
return True
def filtered_queryset(self):
return self.model.objects.all()
def can_start(self, obj, validate_license=True):
return False
class WorkflowApprovalTemplateAccess(BaseAccess):
'''
I can approve workflows when:
- I'm authenticated
I can create when:
- I'm a superuser:
'''
model = WorkflowApprovalTemplate
prefetch_related = ('created_by', 'modified_by',)
def can_read(self, obj):
return True
def can_use(self, obj):
return True
def filtered_queryset(self):
return self.model.objects.all()
for cls in BaseAccess.__subclasses__(): for cls in BaseAccess.__subclasses__():
access_registry[cls.model] = cls access_registry[cls.model] = cls

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 19:41 # Generated by Django 1.11.20 on 2019-07-03 14:38
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@@ -33,4 +33,9 @@ class Migration(migrations.Migration):
}, },
bases=('main.unifiedjobtemplate',), bases=('main.unifiedjobtemplate',),
), ),
migrations.AddField(
model_name='workflowapproval',
name='workflow_approval_template',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approvals', to='main.WorkflowApprovalTemplate'),
),
] ]

View File

@@ -55,7 +55,7 @@ from awx.main.models.notifications import ( # noqa
from awx.main.models.label import Label # noqa from awx.main.models.label import Label # noqa
from awx.main.models.workflow import ( # noqa from awx.main.models.workflow import ( # noqa
WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate,
WorkflowJobTemplateNode, WorkflowJobTemplateNode, WorkflowApproval, WorkflowApprovalTemplate,
) )
from awx.main.models.channels import ChannelGroup # noqa from awx.main.models.channels import ChannelGroup # noqa
from awx.api.versioning import reverse from awx.api.versioning import reverse
@@ -212,4 +212,3 @@ prevent_search(RefreshToken._meta.get_field('token'))
prevent_search(OAuth2Application._meta.get_field('client_secret')) prevent_search(OAuth2Application._meta.get_field('client_secret'))
prevent_search(OAuth2Application._meta.get_field('client_id')) prevent_search(OAuth2Application._meta.get_field('client_id'))
prevent_search(Grant._meta.get_field('code')) prevent_search(Grant._meta.get_field('code'))

View File

@@ -34,6 +34,7 @@ from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemp
from awx.main.models.credential import Credential from awx.main.models.credential import Credential
from awx.main.redact import REPLACE_STR from awx.main.redact import REPLACE_STR
from awx.main.fields import JSONField from awx.main.fields import JSONField
from awx.main.utils import schedule_task_manager
from copy import copy from copy import copy
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -604,7 +605,6 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
class WorkflowApprovalTemplate(UnifiedJobTemplate): class WorkflowApprovalTemplate(UnifiedJobTemplate):
class Meta: class Meta:
app_label = 'main' app_label = 'main'
@@ -616,8 +616,42 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate):
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
return ['name', 'description'] return ['name', 'description']
def get_absolute_url(self, request=None):
return reverse('api:workflow_approval_template_detail', kwargs={'pk': self.pk}, request=request)
class WorkflowApproval(UnifiedJob): class WorkflowApproval(UnifiedJob):
class Meta: class Meta:
app_label = 'main' app_label = 'main'
workflow_approval_template = models.ForeignKey(
'WorkflowApprovalTemplate',
related_name='approvals',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
@classmethod
def _get_unified_job_template_class(cls):
return WorkflowApprovalTemplate
def get_absolute_url(self, request=None):
return reverse('api:workflow_approval_detail', kwargs={'pk': self.pk}, request=request)
@property
def event_class(self):
return None
def approve(self, request=None):
self.status = 'successful'
self.save()
schedule_task_manager()
return reverse('api:approved_workflow', kwargs={'pk': self.pk}, request=request)
def reject(self, request=None):
self.status = 'failed'
self.save()
schedule_task_manager()
return reverse('api:rejected_workflow', kwargs={'pk': self.pk}, request=request)

View File

@@ -10,7 +10,7 @@ from django.contrib.contenttypes.models import ContentType
# AWX # AWX
from awx.main.models import ( from awx.main.models import (
UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate, UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate,
Project, WorkflowJob, Schedule, WorkflowApprovalTemplate, Project, WorkflowJob, Schedule,
Credential Credential
) )
@@ -20,7 +20,9 @@ def test_subclass_types(rando):
assert set(UnifiedJobTemplate._submodels_with_roles()) == set([ assert set(UnifiedJobTemplate._submodels_with_roles()) == set([
ContentType.objects.get_for_model(JobTemplate).id, ContentType.objects.get_for_model(JobTemplate).id,
ContentType.objects.get_for_model(Project).id, ContentType.objects.get_for_model(Project).id,
ContentType.objects.get_for_model(WorkflowJobTemplate).id ContentType.objects.get_for_model(WorkflowJobTemplate).id,
ContentType.objects.get_for_model(WorkflowApprovalTemplate).id
]) ])