diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 0d9867f49c..8073b89735 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3011,7 +3011,7 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO fields = ('*', 'host_config_key', 'ask_diff_mode_on_launch', 'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled', 'become_enabled', 'diff_mode', - 'allow_simultaneous', 'custom_virtualenv') + 'allow_simultaneous', 'custom_virtualenv', 'job_shard_count') def get_related(self, obj): res = super(JobTemplateSerializer, self).get_related(obj) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index e1af329953..de8756ce40 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -2903,7 +2903,7 @@ class JobTemplateLaunch(RetrieveAPIView): raise PermissionDenied() passwords = serializer.validated_data.pop('credential_passwords', {}) - new_job = obj.create_unified_job(**serializer.validated_data) + new_job = obj.create_job(**serializer.validated_data) result = new_job.signal_start(**passwords) if not result: @@ -2914,7 +2914,10 @@ class JobTemplateLaunch(RetrieveAPIView): data = OrderedDict() data['job'] = new_job.id data['ignored_fields'] = self.sanitize_for_response(ignored_fields) - data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) + if isinstance(new_job, WorkflowJob): + data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) + else: + data.update(JobSerializer(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) diff --git a/awx/main/migrations/0048_v340_split_jobs.py b/awx/main/migrations/0048_v340_split_jobs.py new file mode 100644 index 0000000000..de1242760a --- /dev/null +++ b/awx/main/migrations/0048_v340_split_jobs.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-08-14 13:43 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0047_v330_activitystream_instance'), + ] + + operations = [ + migrations.AddField( + model_name='jobtemplate', + name='job_shard_count', + field=models.IntegerField(blank=True, + default=0, + help_text='The number of jobs to split into at runtime. Will cause the Job Template to launch a workflow.'), + ), + ] diff --git a/awx/main/migrations/0049_v340_add_job_template.py b/awx/main/migrations/0049_v340_add_job_template.py new file mode 100644 index 0000000000..3174ca9532 --- /dev/null +++ b/awx/main/migrations/0049_v340_add_job_template.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-08-14 16:04 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0048_v340_split_jobs'), + ] + + operations = [ + migrations.AddField( + model_name='workflowjob', + name='job_template', + field=models.ForeignKey(blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='sharded_jobs', to='main.JobTemplate'), + ), + ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index c5bad0c6c4..79bd353832 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -277,6 +277,12 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour default=False, allows_field='credentials' ) + job_shard_count = models.IntegerField( + blank=True, + default=0, + help_text=_("The number of jobs to split into at runtime. Will cause the Job Template to launch a workflow."), + ) + admin_role = ImplicitRoleField( parent_role=['project.organization.job_template_admin_role', 'inventory.organization.job_template_admin_role'] ) @@ -318,6 +324,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour ''' Create a new job based on this template. ''' + if self.job_shard_count > 1: + # A sharded Job Template will generate a WorkflowJob rather than a Job + from awx.main.models.workflow import WorkflowJobTemplate + kwargs['_unified_job_class'] = WorkflowJobTemplate._get_unified_job_class() + kwargs['_unified_job_field_names'] = WorkflowJobTemplate._get_unified_job_field_names() return self.create_unified_job(**kwargs) def get_absolute_url(self, request=None): diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index da3f43ad80..54b6dbad3e 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -328,6 +328,8 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio ''' Create a new unified job based on this unified job template. ''' + from awx.main.models import JobTemplate, WorkflowJob + new_job_passwords = kwargs.pop('survey_passwords', {}) eager_fields = kwargs.pop('_eager_fields', None) @@ -336,8 +338,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio password_list = self.survey_password_variables() encrypt_dict(kwargs.get('extra_vars', {}), password_list) - unified_job_class = self._get_unified_job_class() - fields = self._get_unified_job_field_names() + unified_job_class = kwargs.pop("_unified_job_class", self._get_unified_job_class()) + fields = kwargs.pop("_unified_job_field_names", self._get_unified_job_field_names()) + print("UJC: {}".format(unified_job_class)) + print("fields: {}".format(fields)) unallowed_fields = set(kwargs.keys()) - set(fields) if unallowed_fields: logger.warn('Fields {} are not allowed as overrides.'.format(unallowed_fields)) @@ -350,7 +354,11 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio setattr(unified_job, fd, val) # Set the unified job template back-link on the job - parent_field_name = unified_job_class._get_parent_field_name() + # TODO: fix this hack properly before merge matburt + if isinstance(self, JobTemplate) and isinstance(unified_job, WorkflowJob): + parent_field_name = "job_template" + else: + parent_field_name = unified_job_class._get_parent_field_name() setattr(unified_job, parent_field_name, self) # For JobTemplate-based jobs with surveys, add passwords to list for perma-redaction diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 357dd9eeb0..b97f555d57 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -433,6 +433,14 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio default=None, on_delete=models.SET_NULL, ) + job_template = models.ForeignKey( + 'JobTemplate', + related_name='sharded_jobs', + blank=True, + null=True, + default=None, + on_delete=models.SET_NULL, + ) @property def workflow_nodes(self):