mirror of
https://github.com/ansible/awx.git
synced 2026-03-04 02:01:01 -03:30
Add ability to prompt for several variable types on launch
This commit is contained in:
@@ -1673,7 +1673,9 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JobTemplate
|
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):
|
def get_related(self, obj):
|
||||||
res = super(JobTemplateSerializer, self).get_related(obj)
|
res = super(JobTemplateSerializer, self).get_related(obj)
|
||||||
@@ -1730,10 +1732,16 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
|
|
||||||
passwords_needed_to_start = serializers.ReadOnlyField()
|
passwords_needed_to_start = serializers.ReadOnlyField()
|
||||||
ask_variables_on_launch = 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:
|
class Meta:
|
||||||
model = Job
|
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):
|
def get_related(self, obj):
|
||||||
res = super(JobSerializer, self).get_related(obj)
|
res = super(JobSerializer, self).get_related(obj)
|
||||||
@@ -2102,9 +2110,13 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
fields = ('can_start_without_user_input', 'passwords_needed_to_start', 'extra_vars',
|
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',)
|
'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 = {
|
extra_kwargs = {
|
||||||
'credential': {
|
'credential': {
|
||||||
'write_only': True,
|
'write_only': True,
|
||||||
@@ -2164,7 +2176,9 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
if errors:
|
if errors:
|
||||||
raise serializers.ValidationError(errors)
|
raise serializers.ValidationError(errors)
|
||||||
|
|
||||||
|
JT_extra_vars = obj.extra_vars
|
||||||
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
||||||
|
obj.extra_vars = JT_extra_vars
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
class NotifierSerializer(BaseSerializer):
|
class NotifierSerializer(BaseSerializer):
|
||||||
|
|||||||
@@ -2115,8 +2115,60 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
|
|||||||
kv = {
|
kv = {
|
||||||
'credential': serializer.instance.credential.pk,
|
'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:
|
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)
|
kv.update(passwords)
|
||||||
|
|
||||||
new_job = obj.create_unified_job(**kv)
|
new_job = obj.create_unified_job(**kv)
|
||||||
@@ -2127,6 +2179,9 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
|
|||||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
data = dict(job=new_job.id)
|
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)
|
return Response(data, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
|
||||||
|
|||||||
35
awx/main/migrations/0013_v300_job_prompt_for_fields.py
Normal file
35
awx/main/migrations/0013_v300_job_prompt_for_fields.py
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -194,6 +194,22 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
|
|||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
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(
|
survey_enabled = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
|
|||||||
159
awx/main/tests/functional/api/test_job_runtime_params.py
Normal file
159
awx/main/tests/functional/api/test_job_runtime_params.py
Normal file
@@ -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
|
||||||
@@ -167,6 +167,10 @@ def organization_factory(instance):
|
|||||||
def credential():
|
def credential():
|
||||||
return Credential.objects.create(kind='aws', name='test-cred')
|
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
|
@pytest.fixture
|
||||||
def inventory(organization):
|
def inventory(organization):
|
||||||
return organization.inventories.create(name="test-inv")
|
return organization.inventories.create(name="test-inv")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
|
|||||||
project = self.proj_dev.pk,
|
project = self.proj_dev.pk,
|
||||||
credential = self.cred_sue.pk,
|
credential = self.cred_sue.pk,
|
||||||
playbook = self.proj_dev.playbooks[0],
|
playbook = self.proj_dev.playbooks[0],
|
||||||
|
ask_variables_on_launch = True,
|
||||||
)
|
)
|
||||||
self.data_no_cred = dict(
|
self.data_no_cred = dict(
|
||||||
name = 'launched job template no credential',
|
name = 'launched job template no credential',
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ else
|
|||||||
echo "Failed to find tower source tree, map your development tree volume"
|
echo "Failed to find tower source tree, map your development tree volume"
|
||||||
fi
|
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
|
# Check if we need to build dependencies
|
||||||
#if [ -f "awx/lib/.deps_built" ]; then
|
#if [ -f "awx/lib/.deps_built" ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user