diff --git a/awx/main/migrations/0167_jt_prompt_everything_on_launch.py b/awx/main/migrations/0167_jt_prompt_everything_on_launch.py index 9bd3736c70..2a44084a2f 100644 --- a/awx/main/migrations/0167_jt_prompt_everything_on_launch.py +++ b/awx/main/migrations/0167_jt_prompt_everything_on_launch.py @@ -214,4 +214,24 @@ class Migration(migrations.Migration): to='main.InstanceGroup', ), ), + migrations.CreateModel( + name='WorkflowJobInstanceGroupMembership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('position', models.PositiveIntegerField(db_index=True, default=None, null=True)), + ('instancegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.instancegroup')), + ('workflowjobnode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.workflowjob')), + ], + ), + migrations.AddField( + model_name='workflowjob', + name='instance_groups', + field=awx.main.fields.OrderedManyToManyField( + blank=True, + editable=False, + related_name='workflow_job_instance_groups', + through='main.WorkflowJobInstanceGroupMembership', + to='main.InstanceGroup', + ), + ), ] diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index 5e51284299..9509523e77 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -489,3 +489,14 @@ class WorkflowJobNodeBaseInstanceGroupMembership(models.Model): default=None, db_index=True, ) + + +class WorkflowJobInstanceGroupMembership(models.Model): + + workflowjobnode = models.ForeignKey('WorkflowJob', on_delete=models.CASCADE) + instancegroup = models.ForeignKey('InstanceGroup', on_delete=models.CASCADE) + position = models.PositiveIntegerField( + null=True, + default=None, + db_index=True, + ) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index bfefcb8f83..63cbb89842 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -943,6 +943,28 @@ class LaunchTimeConfigBase(BaseModel): # This is a solution to the nullable CharField problem, specific to prompting char_prompts = JSONBlob(default=dict, blank=True) + # Define fields that are not really fields, but alias to char_prompts lookups + limit = NullablePromptPseudoField('limit') + scm_branch = NullablePromptPseudoField('scm_branch') + job_tags = NullablePromptPseudoField('job_tags') + skip_tags = NullablePromptPseudoField('skip_tags') + diff_mode = NullablePromptPseudoField('diff_mode') + job_type = NullablePromptPseudoField('job_type') + verbosity = NullablePromptPseudoField('verbosity') + forks = NullablePromptPseudoField('forks') + job_slice_count = NullablePromptPseudoField('job_slice_count') + timeout = NullablePromptPseudoField('timeout') + + # NOTE: additional fields are assumed to exist but must be defined in subclasses + # due to technical limitations + SUBCLASS_FIELDS = ( + 'instance_groups', # needs a through model defined + 'extra_vars', # alternates between extra_vars and extra_data + 'credentials', # already a unified job and unified JT field + 'labels', # already a unified job and unified JT field + 'execution_environment', # already a unified job and unified JT field + ) + def prompts_dict(self, display=False): data = {} # Some types may have different prompts, but always subset of JT prompts @@ -977,15 +999,6 @@ class LaunchTimeConfigBase(BaseModel): return data -for field_name in JobTemplate.get_ask_mapping().keys(): - if field_name == 'extra_vars': - continue - try: - LaunchTimeConfigBase._meta.get_field(field_name) - except FieldDoesNotExist: - setattr(LaunchTimeConfigBase, field_name, NullablePromptPseudoField(field_name)) - - class LaunchTimeConfig(LaunchTimeConfigBase): """ Common model for all objects that save details of a saved launch config @@ -1004,12 +1017,9 @@ class LaunchTimeConfig(LaunchTimeConfigBase): blank=True, ) ) - # Credentials needed for non-unified job / unified JT models + # Fields needed for non-unified job / unified JT models, because they are defined on unified models credentials = models.ManyToManyField('Credential', related_name='%(class)ss') - - # Labels needed for non-unified job / unified JT models labels = models.ManyToManyField('Label', related_name='%(class)s_labels') - execution_environment = models.ForeignKey( 'ExecutionEnvironment', null=True, blank=True, default=None, on_delete=polymorphic.SET_NULL, related_name='%(class)s_as_prompt' ) diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 7330128873..9bc0e3408d 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -384,6 +384,10 @@ class WorkflowJobOptions(LaunchTimeConfigBase): ) ) ) + # Workflow jobs are used for sliced jobs, and thus, must be a conduit for any JT prompts + instance_groups = OrderedManyToManyField( + 'InstanceGroup', related_name='workflow_job_instance_groups', blank=True, editable=False, through='WorkflowJobInstanceGroupMembership' + ) allow_simultaneous = models.BooleanField(default=False) extra_vars_dict = VarsDictProperty('extra_vars', True) diff --git a/awx/main/tests/functional/models/test_job_launch_config.py b/awx/main/tests/functional/models/test_job_launch_config.py index 208a1d7614..a25d99b22a 100644 --- a/awx/main/tests/functional/models/test_job_launch_config.py +++ b/awx/main/tests/functional/models/test_job_launch_config.py @@ -1,7 +1,8 @@ import pytest # AWX -from awx.main.models import JobTemplate, JobLaunchConfig, ExecutionEnvironment +from awx.main.models.jobs import JobTemplate, JobLaunchConfig, LaunchTimeConfigBase +from awx.main.models.execution_environments import ExecutionEnvironment @pytest.fixture @@ -75,3 +76,28 @@ class TestConfigReversibility: print(prompts) print(config.prompts_dict()) assert config.prompts_dict() == prompts + + +@pytest.mark.django_db +class TestLaunchConfigModels: + def get_concrete_subclasses(self, cls): + r = [] + for c in cls.__subclasses__(): + if c._meta.abstract: + r.extend(self.get_concrete_subclasses(c)) + else: + r.append(c) + return r + + def test_non_job_config_complete(self): + """This performs model validation which replaces code that used run on import.""" + for field_name in JobTemplate.get_ask_mapping().keys(): + if field_name in LaunchTimeConfigBase.SUBCLASS_FIELDS: + assert not hasattr(LaunchTimeConfigBase, field_name) + else: + assert hasattr(LaunchTimeConfigBase, field_name) + + def test_subclass_fields_complete(self): + for cls in self.get_concrete_subclasses(LaunchTimeConfigBase): + for field_name in LaunchTimeConfigBase.SUBCLASS_FIELDS: + assert hasattr(cls, field_name)