From cc84ed51d6254d46f287244ffdbf1afcaaaf2f4a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 4 Apr 2016 15:45:47 -0400 Subject: [PATCH] Add ability to prompt for several variable types on launch --- awx/api/serializers.py | 22 ++- awx/api/views.py | 57 ++++++- .../0013_v300_job_prompt_for_fields.py | 35 ++++ awx/main/models/jobs.py | 16 ++ .../functional/api/test_job_runtime_params.py | 159 ++++++++++++++++++ awx/main/tests/functional/conftest.py | 4 + awx/main/tests/old/jobs/job_launch.py | 1 + tools/docker-compose/start_development.sh | 3 +- 8 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 awx/main/migrations/0013_v300_job_prompt_for_fields.py create mode 100644 awx/main/tests/functional/api/test_job_runtime_params.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bda63004c4..2bc5c7a151 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1673,7 +1673,9 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): class Meta: model = JobTemplate - fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'survey_enabled', 'become_enabled') + fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'ask_limit_on_launch', + 'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_inventory_on_launch', + 'survey_enabled', 'become_enabled') def get_related(self, obj): res = super(JobTemplateSerializer, self).get_related(obj) @@ -1730,10 +1732,16 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): passwords_needed_to_start = serializers.ReadOnlyField() ask_variables_on_launch = serializers.ReadOnlyField() + ask_limit_on_launch = serializers.ReadOnlyField() + ask_tags_on_launch = serializers.ReadOnlyField() + ask_job_type_on_launch = serializers.ReadOnlyField() + ask_inventory_on_launch = serializers.ReadOnlyField() class Meta: model = Job - fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch') + fields = ('*', 'job_template', 'passwords_needed_to_start', 'ask_variables_on_launch', + 'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch', + 'ask_inventory_on_launch') def get_related(self, obj): res = super(JobSerializer, self).get_related(obj) @@ -2102,9 +2110,13 @@ class JobLaunchSerializer(BaseSerializer): class Meta: model = JobTemplate fields = ('can_start_without_user_input', 'passwords_needed_to_start', 'extra_vars', - 'ask_variables_on_launch', 'survey_enabled', 'variables_needed_to_start', + 'ask_variables_on_launch', 'ask_tags_on_launch', 'ask_job_type_on_launch', + 'ask_inventory_on_launch', 'ask_limit_on_launch', + 'survey_enabled', 'variables_needed_to_start', 'credential', 'credential_needed_to_start',) - read_only_fields = ('ask_variables_on_launch',) + read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch', + 'ask_tags_on_launch', 'ask_job_type_on_launch', + 'ask_inventory_on_launch') extra_kwargs = { 'credential': { 'write_only': True, @@ -2164,7 +2176,9 @@ class JobLaunchSerializer(BaseSerializer): if errors: raise serializers.ValidationError(errors) + JT_extra_vars = obj.extra_vars attrs = super(JobLaunchSerializer, self).validate(attrs) + obj.extra_vars = JT_extra_vars return attrs class NotifierSerializer(BaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index adb16289c1..b1243c28a7 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2115,8 +2115,60 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): kv = { 'credential': serializer.instance.credential.pk, } + + # -- following code will be moved to JobTemplate model -- + # Sort the runtime fields allowed and disallowed by job template + ignored_fields = {} if 'extra_vars' in request.data: - kv['extra_vars'] = request.data['extra_vars'] + kv['extra_vars'] = {} + ignored_fields['extra_vars'] = {} + if obj.ask_variables_on_launch: + # Accept all extra_vars if the flag is set + kv['extra_vars'] = request.data['extra_vars'] + else: + if obj.survey_enabled: + # Accept vars defined in the survey and no others + survey_vars = [question['variable'] for question in obj.survey_spec['spec']] + for key in request.data['extra_vars']: + if key in survey_vars: + kv['extra_vars'][key] = request.data['extra_vars'][key] + else: + ignored_fields['extra_vars'][key] = request.data['extra_vars'][key] + else: + # No survey & prompt flag is false - ignore all + ignored_fields['extra_vars'] = request.data['extra_vars'] + + if 'limit' in request.data: + if obj.ask_limit_on_launch: + kv['limit'] = request.data['limit'] + else: + ignored_fields['limit'] = request.data['limit'] + + if 'job_tags' or 'skip_tags' in request.data: + if obj.ask_tags_on_launch: + if 'job_tags' in request.data: + kv['job_tags'] = request.data['job_tags'] + if 'skip_tags' in request.data: + kv['skip_tags'] = request.data['skip_tags'] + else: + if 'job_tags' in request.data: + ignored_fields['job_tags'] = request.data['job_tags'] + if 'skip_tags' in request.data: + ignored_fields['skip_tags'] = request.data['skip_tags'] + + if 'job_type' in request.data: + if obj.ask_job_type_on_launch: + kv['job_type'] = request.data['job_type'] + else: + ignored_fields['job_type'] = request.data['job_type'] + + if 'inventory' in request.data: + inv_id = request.data['inventory'] + if obj.ask_inventory_on_launch and Inventory.objects.get(pk=inv_id).accessible_by(self.request.user, {'write': True}): + kv['inventory'] = inv_id + else: + ignored_fields['inventory'] = inv_id + kv.update(passwords) new_job = obj.create_unified_job(**kv) @@ -2127,6 +2179,9 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): return Response(data, status=status.HTTP_400_BAD_REQUEST) else: data = dict(job=new_job.id) + serializer = JobSerializer(new_job) + data['job_data'] = serializer.data + data['ignored_fields'] = ignored_fields return Response(data, status=status.HTTP_202_ACCEPTED) class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView): diff --git a/awx/main/migrations/0013_v300_job_prompt_for_fields.py b/awx/main/migrations/0013_v300_job_prompt_for_fields.py new file mode 100644 index 0000000000..f9122b7cee --- /dev/null +++ b/awx/main/migrations/0013_v300_job_prompt_for_fields.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0012_v300_create_labels'), + ] + + operations = [ + migrations.AddField( + model_name='jobtemplate', + name='ask_limit_on_launch', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='jobtemplate', + name='ask_inventory_on_launch', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='jobtemplate', + name='ask_job_type_on_launch', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='jobtemplate', + name='ask_tags_on_launch', + field=models.BooleanField(default=False), + ), + ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0314a57fb7..9a0c62f58a 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -194,6 +194,22 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin): blank=True, default=False, ) + ask_limit_on_launch = models.BooleanField( + blank=True, + default=False, + ) + ask_tags_on_launch = models.BooleanField( + blank=True, + default=False, + ) + ask_job_type_on_launch = models.BooleanField( + blank=True, + default=False, + ) + ask_inventory_on_launch = models.BooleanField( + blank=True, + default=False, + ) survey_enabled = models.BooleanField( default=False, diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py new file mode 100644 index 0000000000..e79c956084 --- /dev/null +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -0,0 +1,159 @@ +import pytest +import yaml + +from awx.api.serializers import JobLaunchSerializer +from awx.main.models.credential import Credential +from awx.main.models.inventory import Inventory +from awx.main.models.jobs import Job, JobTemplate + +from django.core.urlresolvers import reverse + +@pytest.fixture +def runtime_data(): + cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2') + return dict( + limit='test-servers', + job_type='check', + inventory=cred_obj.pk, + job_tags='["provision"]', + skip_tags='["restart"]', + extra_vars='{"job_launch_var": 4}' + ) + +@pytest.fixture +def job_template_prompts(project, inventory, machine_credential): + def rf(on_off): + return JobTemplate.objects.create( + job_type='run', project=project, inventory=inventory, + credential=machine_credential, name='deploy-job-template', + ask_variables_on_launch=on_off, + ask_tags_on_launch=on_off, + ask_job_type_on_launch=on_off, + ask_inventory_on_launch=on_off, + ask_limit_on_launch=on_off, + ) + return rf + +# Probably remove this test after development is finished +@pytest.mark.django_db +def test_job_launch_prompts_echo(job_template_prompts, get, user): + job_template = job_template_prompts(True) + assert job_template.ask_variables_on_launch + + url = reverse('api:job_template_launch', args=[job_template.pk]) + + response = get( + reverse('api:job_template_launch', args=[job_template.pk]), + user('admin', True)) + + # Just checking that the GET response has what we expect + assert response.data['ask_variables_on_launch'] + assert response.data['ask_tags_on_launch'] + assert response.data['ask_job_type_on_launch'] + assert response.data['ask_inventory_on_launch'] + assert response.data['ask_limit_on_launch'] + +@pytest.mark.django_db +def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, user): + job_template = job_template_prompts(False) + + response = post( + reverse('api:job_template_launch', args=[job_template.pk]), + runtime_data, user('admin', True)) + + job_id = response.data['job'] + job_obj = Job.objects.get(pk=job_id) + + # Check that job data matches job_template data + assert len(yaml.load(job_obj.extra_vars)) == 0 + assert job_obj.limit == job_template.limit + assert job_obj.job_type == job_template.job_type + assert job_obj.inventory.pk == job_template.inventory.pk + assert job_obj.job_tags == job_template.job_tags + + # Check that response tells us what things were ignored + assert 'job_launch_var' in response.data['ignored_fields']['extra_vars'] + assert 'job_type' in response.data['ignored_fields'] + assert 'limit' in response.data['ignored_fields'] + assert 'inventory' in response.data['ignored_fields'] + assert 'job_tags' in response.data['ignored_fields'] + assert 'skip_tags' in response.data['ignored_fields'] + +@pytest.mark.django_db +def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user): + job_template = job_template_prompts(True) + + response = post( + reverse('api:job_template_launch', args=[job_template.pk]), + runtime_data, user('admin', True)) + + job_id = response.data['job'] + job_obj = Job.objects.get(pk=job_id) + + # Check that job data matches the given runtime variables + assert 'job_launch_var' in yaml.load(job_obj.extra_vars) + assert job_obj.limit == runtime_data['limit'] + assert job_obj.job_type == runtime_data['job_type'] + assert job_obj.inventory.pk == runtime_data['inventory'] + assert job_obj.job_tags == runtime_data['job_tags'] + +@pytest.mark.django_db +def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate): + deploy_jobtemplate.extra_vars = '{"job_template_var": 3}' + deploy_jobtemplate.save() + + kv = dict(extra_vars={"job_launch_var": 4}, credential=machine_credential.id) + serializer = JobLaunchSerializer( + instance=deploy_jobtemplate, data=kv, + context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}}) + validated = serializer.is_valid() + assert validated + + job_obj = deploy_jobtemplate.create_unified_job(**kv) + result = job_obj.signal_start(**kv) + + final_job_extra_vars = yaml.load(job_obj.extra_vars) + assert result + assert 'job_template_var' in final_job_extra_vars + assert 'job_launch_var' in final_job_extra_vars + +@pytest.mark.django_db +def test_job_launch_unprompted_vars_with_survey(job_template_prompts, post, user): + job_template = job_template_prompts(False) + job_template.survey_enabled = True + job_template.survey_spec = { + "spec": [ + { + "index": 0, + "question_name": "survey_var", + "min": 0, + "default": "", + "max": 100, + "question_description": "A survey question", + "required": True, + "variable": "survey_var", + "choices": "", + "type": "integer" + } + ], + "description": "", + "name": "" + } + job_template.save() + + response = post( + reverse('api:job_template_launch', args=[job_template.pk]), + dict(extra_vars={"job_launch_var": 3, "survey_var": 4}), + user('admin', True)) + + job_id = response.data['job'] + job_obj = Job.objects.get(pk=job_id) + + # Check that the survey variable is accept and the job variable isn't + job_extra_vars = yaml.load(job_obj.extra_vars) + assert 'job_launch_var' not in job_extra_vars + assert 'survey_var' in job_extra_vars + +# To add: +# permissions testing (can't provide inventory you can't run against) +# credentials/password test if they will be included in response format diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 593f3e420b..e187d8cd03 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -167,6 +167,10 @@ def organization_factory(instance): def credential(): return Credential.objects.create(kind='aws', name='test-cred') +@pytest.fixture +def machine_credential(): + return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word') + @pytest.fixture def inventory(organization): return organization.inventories.create(name="test-inv") diff --git a/awx/main/tests/old/jobs/job_launch.py b/awx/main/tests/old/jobs/job_launch.py index c0997607ee..57c87a1b82 100644 --- a/awx/main/tests/old/jobs/job_launch.py +++ b/awx/main/tests/old/jobs/job_launch.py @@ -27,6 +27,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase): project = self.proj_dev.pk, credential = self.cred_sue.pk, playbook = self.proj_dev.playbooks[0], + ask_variables_on_launch = True, ) self.data_no_cred = dict( name = 'launched job template no credential', diff --git a/tools/docker-compose/start_development.sh b/tools/docker-compose/start_development.sh index 16d859a3ce..30557418b7 100755 --- a/tools/docker-compose/start_development.sh +++ b/tools/docker-compose/start_development.sh @@ -19,7 +19,8 @@ else echo "Failed to find tower source tree, map your development tree volume" fi -cp -nR /tmp/ansible_tower.egg-info /tower_devel/ || true +# will remove before PR lands +cp -fR /tmp/ansible_tower.egg-info /tower_devel/ || true # Check if we need to build dependencies #if [ -f "awx/lib/.deps_built" ]; then