diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bb964f2be0..9f8b8835ea 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2252,6 +2252,7 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer): res['workflow_nodes'] = reverse('api:workflow_job_workflow_nodes_list', args=(obj.pk,)) res['labels'] = reverse('api:workflow_job_label_list', args=(obj.pk,)) res['activity_stream'] = reverse('api:workflow_job_activity_stream_list', args=(obj.pk,)) + res['relaunch'] = reverse('api:workflow_job_relaunch', args=(obj.pk,)) if obj.can_cancel or True: res['cancel'] = reverse('api:workflow_job_cancel', args=(obj.pk,)) return res diff --git a/awx/api/urls.py b/awx/api/urls.py index b155bd63a6..170d750baa 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -272,7 +272,6 @@ workflow_job_template_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/notification_templates_success/$', 'workflow_job_template_notification_templates_success_list'), url(r'^(?P[0-9]+)/access_list/$', 'workflow_job_template_access_list'), url(r'^(?P[0-9]+)/labels/$', 'workflow_job_template_label_list'), -# url(r'^(?P[0-9]+)/cancel/$', 'workflow_job_template_cancel'), ) workflow_job_urls = patterns('awx.api.views', @@ -281,6 +280,7 @@ workflow_job_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/workflow_nodes/$', 'workflow_job_workflow_nodes_list'), url(r'^(?P[0-9]+)/labels/$', 'workflow_job_label_list'), url(r'^(?P[0-9]+)/cancel/$', 'workflow_job_cancel'), + url(r'^(?P[0-9]+)/relaunch/$', 'workflow_job_relaunch'), url(r'^(?P[0-9]+)/notifications/$', 'workflow_job_notifications_list'), url(r'^(?P[0-9]+)/activity_stream/$', 'workflow_job_activity_stream_list'), ) diff --git a/awx/api/views.py b/awx/api/views.py index b699be976f..f502576da3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2987,6 +2987,25 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): return Response(data, status=status.HTTP_201_CREATED) +class WorkflowJobRelaunch(GenericAPIView): + + model = WorkflowJob + serializer_class = EmptySerializer + is_job_start = True + + def get(self, request, *args, **kwargs): + return Response({}) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + new_workflow_job = obj.create_relaunch_workflow_job() + result = new_workflow_job.signal_start() + + data = WorkflowJobSerializer(new_workflow_job, context=self.get_serializer_context()).data + headers = {'Location': new_workflow_job.get_absolute_url()} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) + + # TODO: class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index 57f46b8242..4b41da80ba 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1630,9 +1630,16 @@ class WorkflowJobAccess(BaseAccess): return self.user.is_superuser return self.user in obj.workflow_job_template.admin_role - # TODO: add support for relaunching workflow jobs def can_start(self, obj, validate_license=True): - return False + if validate_license: + self.check_license() + if obj.survey_enabled: + self.check_license(feature='surveys') + + if self.user.is_superuser: + return True + + return (obj.workflow_job_template and self.user in obj.workflow_job_template.execute_role) def can_cancel(self, obj): if not obj.can_cancel: diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 34f811d0b4..7e9fc54bcc 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -455,6 +455,10 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin): def _global_timeout_setting(self): return 'DEFAULT_JOB_TIMEOUT' + @classmethod + def _get_unified_job_template_class(cls): + return JobTemplate + def get_absolute_url(self): return reverse('api:job_detail', args=(self.pk,)) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 51ca7765fb..8dcb796513 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -357,7 +357,6 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio dest_field.add(*list(src_field_value.all().values_list('id', flat=True))) return unified_job - def copy_unified_jt(self): ''' Create a copy of this unified job template. @@ -585,6 +584,13 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique def _get_parent_field_name(cls): return 'unified_job_template' # Override in subclasses. + @classmethod + def _get_unified_job_template_class(cls): + ''' + Return subclass of UnifiedJobTemplate that applies to this unified job. + ''' + raise NotImplementedError # Implement in subclass. + def _global_timeout_setting(self): "Override in child classes, None value indicates this is not configurable" return None @@ -699,6 +705,36 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique pass super(UnifiedJob, self).delete() + def copy_unified_job(self): + ''' + Create a copy of this unified job. + ''' + unified_job_class = self.__class__ + unified_jt_class = self._get_unified_job_template_class() + create_kwargs = {} + m2m_fields = {} + for field_name in unified_jt_class._get_unified_job_field_names(): + # Foreign keys can be specified as field_name or field_name_id. + id_field_name = '%s_id' % field_name + if hasattr(self, id_field_name): + value = getattr(self, id_field_name) + if hasattr(value, 'id'): + value = value.id + create_kwargs[id_field_name] = value + elif hasattr(self, field_name): + field_obj = self._meta.get_field_by_name(field_name)[0] + # Many to Many can be specified as field_name + if isinstance(field_obj, models.ManyToManyField): + m2m_fields[field_name] = getattr(self, field_name) + else: + create_kwargs[field_name] = getattr(self, field_name) + unified_job = unified_job_class(**create_kwargs) + unified_job.save() + for field_name, src_field_value in m2m_fields.iteritems(): + dest_field = getattr(unified_job, field_name) + dest_field.add(*list(src_field_value.all().values_list('id', flat=True))) + return unified_job + def result_stdout_raw_handle(self, attempt=0): """Return a file-like object containing the standard out of the job's result. diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index ef0c05f75d..dcd1fd305b 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -322,6 +322,12 @@ class WorkflowJobOptions(BaseModel): node_links = self._create_workflow_nodes(old_node_list, user=user) self._inherit_node_relationships(old_node_list, node_links) + def create_relaunch_workflow_job(self): + self.launch_type = 'relaunch' + new_workflow_job = self.copy_unified_job() + new_workflow_job.copy_nodes_from_original(original=self) + return new_workflow_job + class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin): class Meta: @@ -424,6 +430,11 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl return new_wfjt +# Stub in place because of old migraitons, can remove if migraitons are squashed +class WorkflowJobInheritNodesMixin(object): + pass + + class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificationMixin): class Meta: app_label = 'main' @@ -446,6 +457,10 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio def _get_parent_field_name(cls): return 'workflow_job_template' + @classmethod + def _get_unified_job_template_class(cls): + return WorkflowJobTemplate + def _has_failed(self): return False