Merge pull request #9 from AlanCoding/11th-hour

Merge devel, update job launch behavior to newest permissions stuff
This commit is contained in:
Akita Noek 2016-04-18 15:03:58 -04:00
commit fa98b59b6c
16 changed files with 639 additions and 101 deletions

View File

@ -1675,7 +1675,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',
'ask_credential_on_launch', 'survey_enabled', 'become_enabled')
def get_related(self, obj):
res = super(JobTemplateSerializer, self).get_related(obj)
@ -1732,10 +1734,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)
@ -2098,24 +2106,36 @@ class JobLaunchSerializer(BaseSerializer):
can_start_without_user_input = serializers.BooleanField(read_only=True)
variables_needed_to_start = serializers.ReadOnlyField()
credential_needed_to_start = serializers.SerializerMethodField()
inventory_needed_to_start = serializers.SerializerMethodField()
survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True)
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',
'credential', 'credential_needed_to_start',)
read_only_fields = ('ask_variables_on_launch',)
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
'credential', '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_needed_to_start', 'inventory_needed_to_start',)
read_only_fields = ('ask_variables_on_launch', 'ask_limit_on_launch',
'ask_tags_on_launch', 'ask_job_type_on_launch',
'ask_inventory_on_launch', 'ask_credential_on_launch')
extra_kwargs = {
'credential': {
'write_only': True,
},
'credential': {'write_only': True,},
'limit': {'write_only': True,},
'job_tags': {'write_only': True,},
'skip_tags': {'write_only': True,},
'job_type': {'write_only': True,},
'inventory': {'write_only': True,}
}
def get_credential_needed_to_start(self, obj):
return not (obj and obj.credential)
def get_inventory_needed_to_start(self, obj):
return not (obj and obj.inventory)
def get_survey_enabled(self, obj):
if obj:
return obj.survey_enabled and 'spec' in obj.survey_spec
@ -2126,7 +2146,10 @@ class JobLaunchSerializer(BaseSerializer):
obj = self.context.get('obj')
data = self.context.get('data')
credential = attrs.get('credential', obj and obj.credential or None)
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
credential = obj.credential
else:
credential = attrs.get('credential', None)
if not credential:
errors['credential'] = 'Credential not provided'
@ -2160,13 +2183,27 @@ class JobLaunchSerializer(BaseSerializer):
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None):
errors['project'] = 'Job Template Project is missing or undefined'
if obj.inventory is None:
if (obj.inventory is None) and not attrs.get('inventory', None):
errors['inventory'] = 'Job Template Inventory is missing or undefined'
if errors:
raise serializers.ValidationError(errors)
JT_extra_vars = obj.extra_vars
JT_limit = obj.limit
JT_job_type = obj.job_type
JT_job_tags = obj.job_tags
JT_skip_tags = obj.skip_tags
JT_inventory = obj.inventory
JT_credential = obj.credential
attrs = super(JobLaunchSerializer, self).validate(attrs)
obj.extra_vars = JT_extra_vars
obj.limit = JT_limit
obj.job_type = JT_job_type
obj.skip_tags = JT_skip_tags
obj.job_tags = JT_job_tags
obj.inventory = JT_inventory
obj.credential = JT_credential
return attrs
class NotifierSerializer(BaseSerializer):

View File

@ -6,24 +6,40 @@ The response will include the following fields:
* `ask_variables_on_launch`: Flag indicating whether the job_template is
configured to prompt for variables upon launch (boolean, read-only)
* `ask_tags_on_launch`: Flag indicating whether the job_template is
configured to prompt for tags upon launch (boolean, read-only)
* `ask_job_type_on_launch`: Flag indicating whether the job_template is
configured to prompt for job_type upon launch (boolean, read-only)
* `ask_limit_on_launch`: Flag indicating whether the job_template is
configured to prompt for limit upon launch (boolean, read-only)
* `ask_inventory_on_launch`: Flag indicating whether the job_template is
configured to prompt for inventory upon launch (boolean, read-only)
* `ask_credential_on_launch`: Flag indicating whether the job_template is
configured to prompt for credential upon launch (boolean, read-only)
* `can_start_without_user_input`: Flag indicating if the job_template can be
launched without user-input (boolean, read-only)
* `passwords_needed_to_start`: Password names required to launch the
job_template (array, read-only)
* `variables_needed_to_start`: Required variable names required to launch the
job_template (array, read-only)
* `survey_enabled`: Flag indicating if whether the job_template has an enabled
* `survey_enabled`: Flag indicating whether the job_template has an enabled
survey (boolean, read-only)
* `credential_needed_to_start`: Flag indicating the presence of a credential
associated with the job template. If not then one should be supplied when
launching the job (boolean, read-only)
* `inventory_needed_to_start`: Flag indicating the presence of an inventory
associated with the job template. If not then one should be supplied when
launching the job (boolean, read-only)
Make a POST request to this resource to launch the job_template. If any
passwords or extra variables (extra_vars) are required, they must be passed
via POST data, with extra_vars given as a YAML or JSON string and escaped
parentheses. If `credential_needed_to_start` is `True` then the `credential`
field is required as well.
passwords, inventory, or extra variables (extra_vars) are required, they must
be passed via POST data, with extra_vars given as a YAML or JSON string and
escaped parentheses. If `credential_needed_to_start` is `True` then the
`credential` field is required and if the `inventory_needed_to_start` is
`True` then the `inventory` is required as well.
If successful, the response status code will be 202. If any required passwords
If successful, the response status code will be 201. If any required passwords
are not provided, a 400 status code will be returned. If the job cannot be
launched, a 405 status code will be returned.
launched, a 405 status code will be returned. If the provided credential or
inventory are not allowed to be used by the user, then a 403 status code will
be returned.

View File

@ -2082,17 +2082,23 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
def update_raw_data(self, data):
obj = self.get_object()
extra_vars = data.get('extra_vars') or {}
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for p in obj.passwords_needed_to_start:
data[p] = u''
if obj.credential:
data.pop('credential', None)
else:
data['credential'] = None
for v in obj.variables_needed_to_start:
extra_vars.setdefault(v, u'')
data['extra_vars'] = extra_vars
if extra_vars:
data['extra_vars'] = extra_vars
ask_for_vars_dict = obj._ask_for_vars_dict()
ask_for_vars_dict.pop('extra_vars')
for field in ask_for_vars_dict:
if not ask_for_vars_dict[field]:
data.pop(field, None)
elif field == 'inventory' or field == 'credential':
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
else:
data[field] = getattr(obj, field)
return data
def post(self, request, *args, **kwargs):
@ -2102,21 +2108,27 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
if 'credential' not in request.data and 'credential_id' in request.data:
request.data['credential'] = request.data['credential_id']
if 'inventory' not in request.data and 'inventory_id' in request.data:
request.data['inventory'] = request.data['inventory_id']
passwords = {}
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# At this point, a credential is gauranteed to exist at serializer.instance.credential
if not request.user.can_access(Credential, 'read', serializer.instance.credential):
raise PermissionDenied()
prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data)
kv = {
'credential': serializer.instance.credential.pk,
}
if 'extra_vars' in request.data:
kv['extra_vars'] = request.data['extra_vars']
if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None):
new_credential = Credential.objects.get(pk=prompted_fields['credential'])
if request.user not in new_credential.use_role:
raise PermissionDenied()
if 'inventory' in prompted_fields and prompted_fields['inventory'] != getattrd(obj, 'inventory.pk', None):
new_inventory = Inventory.objects.get(pk=prompted_fields['inventory'])
if request.user not in new_inventory.use_role:
raise PermissionDenied()
kv = prompted_fields
kv.update(passwords)
new_job = obj.create_unified_job(**kv)
@ -2126,8 +2138,11 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
new_job.delete()
return Response(data, status=status.HTTP_400_BAD_REQUEST)
else:
data = dict(job=new_job.id)
return Response(data, status=status.HTTP_202_ACCEPTED)
data = OrderedDict()
data['ignored_fields'] = ignored_fields
data.update(JobSerializer(new_job).to_representation(new_job))
data['job'] = new_job.id
return Response(data, status=status.HTTP_201_CREATED)
class JobTemplateSchedulesList(SubListCreateAttachDetachAPIView):
@ -2411,7 +2426,7 @@ class JobTemplateCallback(GenericAPIView):
# Return the location of the new job.
headers = {'Location': job.get_absolute_url()}
return Response(status=status.HTTP_202_ACCEPTED, headers=headers)
return Response(status=status.HTTP_201_CREATED, headers=headers)
class JobTemplateJobsList(SubListCreateAPIView):
@ -2459,7 +2474,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
new_job = obj.create_unified_job(**request.data)
new_job.signal_start(**request.data)
data = dict(system_job=new_job.id)
return Response(data, status=status.HTTP_202_ACCEPTED)
return Response(data, status=status.HTTP_201_CREATED)
class SystemJobTemplateSchedulesList(SubListCreateAttachDetachAPIView):

View File

@ -1,27 +0,0 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
from django.core.management.base import BaseCommand
from crum import impersonate
from awx.main.models import User, Organization
class Command(BaseCommand):
"""Creates the default organization if and only if no organizations
exist in the system.
"""
help = 'Creates a default organization iff there are none.'
def handle(self, *args, **kwargs):
# Sanity check: Is there already an organization in the system?
if Organization.objects.count():
return
# Create a default organization as the first superuser found.
try:
superuser = User.objects.filter(is_superuser=True).order_by('pk')[0]
except IndexError:
superuser = None
with impersonate(superuser):
Organization.objects.create(name='Default')
print('Default organization added.')

View File

@ -0,0 +1,47 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved
from django.core.management.base import BaseCommand
from crum import impersonate
from awx.main.models import User, Organization, Project, Inventory, Credential, Host, JobTemplate
class Command(BaseCommand):
"""Create preloaded data, intended for new installs
"""
help = 'Creates a preload tower data iff there is none.'
def handle(self, *args, **kwargs):
# Sanity check: Is there already an organization in the system?
if Organization.objects.count():
return
# Create a default organization as the first superuser found.
try:
superuser = User.objects.filter(is_superuser=True).order_by('pk')[0]
except IndexError:
superuser = None
with impersonate(superuser):
o = Organization.objects.create(name='Default')
p = Project.objects.create(name='Demo Project',
scm_type='git',
scm_url='https://github.com/ansible/ansible-tower-samples',
scm_update_on_launch=True,
scm_update_cache_timeout=0,
organization=o)
c = Credential.objects.create(name='Demo Credential',
username=superuser.username,
created_by=superuser)
c.owner_role.members.add(superuser)
i = Inventory.objects.create(name='Demo Inventory',
organization=o,
created_by=superuser)
Host.objects.create(name='localhost',
inventory=i,
variables="ansible_connection: local",
created_by=superuser)
JobTemplate.objects.create(name='Demo Job Template',
project=p,
inventory=i,
credential=c)
print('Default organization added.')

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('main', '0015_v300_label_changes'),
]
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_credential_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),
),
migrations.AlterField(
model_name='job',
name='inventory',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True),
),
migrations.AlterField(
model_name='jobtemplate',
name='inventory',
field=models.ForeignKey(related_name='jobtemplates', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Inventory', null=True),
),
]

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from awx.main.migrations import _ask_for_variables as ask_for_variables
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0016_v300_prompting_changes'),
]
operations = [
migrations.RunPython(ask_for_variables.migrate_credential),
]

View File

@ -0,0 +1,9 @@
def migrate_credential(apps, schema_editor):
'''If credential is not currently present, set ask_for_credential_on_launch
equal to True, and otherwise leave it as the default False value.
'''
JobTemplate = apps.get_model('main', 'JobTemplate')
for jt in JobTemplate.objects.iterator():
if jt.credential is None:
jt.ask_credential_on_launch = True
jt.save()

View File

@ -53,7 +53,9 @@ class JobOptions(BaseModel):
inventory = models.ForeignKey(
'Inventory',
related_name='%(class)ss',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
project = models.ForeignKey(
@ -194,6 +196,26 @@ 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,
)
ask_credential_on_launch = models.BooleanField(
blank=True,
default=False,
)
survey_enabled = models.BooleanField(
default=False,
@ -236,6 +258,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
'labels',]
def clean(self):
if self.job_type == 'scan' and (self.inventory is None or self.ask_inventory_on_launch):
raise ValidationError('Scan jobs must be assigned a fixed inventory')
if (not self.ask_inventory_on_launch) and self.inventory is None:
raise ValidationError('Job Template must either have an inventory or allow prompting for inventory')
if (not self.ask_credential_on_launch) and self.credential is None:
raise ValidationError('Job Template must either have a credential or allow prompting for credential')
return super(JobTemplate, self).clean()
def create_job(self, **kwargs):
'''
Create a new job based on this template.
@ -250,7 +281,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
Return whether job template can be used to start a new job without
requiring any user input.
'''
return bool(self.credential and not len(self.passwords_needed_to_start) and not len(self.variables_needed_to_start))
return bool(self.credential and not len(self.passwords_needed_to_start) and
not len(self.variables_needed_to_start) and
self.inventory)
@property
def variables_needed_to_start(self):
@ -365,6 +398,50 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
kwargs['extra_vars'] = json.dumps(extra_vars)
return kwargs
def _ask_for_vars_dict(self):
return dict(
extra_vars=self.ask_variables_on_launch,
limit=self.ask_limit_on_launch,
job_tags=self.ask_tags_on_launch,
skip_tags=self.ask_tags_on_launch,
job_type=self.ask_job_type_on_launch,
inventory=self.ask_inventory_on_launch,
credential=self.ask_credential_on_launch
)
def _accept_or_ignore_job_kwargs(self, **kwargs):
# Sort the runtime fields allowed and disallowed by job template
ignored_fields = {}
prompted_fields = {}
ask_for_vars_dict = self._ask_for_vars_dict()
for field in ask_for_vars_dict:
if field in kwargs:
if field == 'extra_vars':
prompted_fields[field] = {}
ignored_fields[field] = {}
if ask_for_vars_dict[field]:
prompted_fields[field] = kwargs[field]
else:
if field == 'extra_vars' and self.survey_enabled:
# Accept vars defined in the survey and no others
survey_vars = [question['variable'] for question in self.survey_spec['spec']]
for key in kwargs[field]:
if key in survey_vars:
prompted_fields[field][key] = kwargs[field][key]
else:
ignored_fields[field][key] = kwargs[field][key]
else:
ignored_fields[field] = kwargs[field]
# Special case to ignore inventory if it is a scan job
if prompted_fields.get('job_type', None) == 'scan' or self.job_type == 'scan':
if 'inventory' in prompted_fields:
ignored_fields['inventory'] = prompted_fields.pop('inventory')
return prompted_fields, ignored_fields
@property
def cache_timeout_blocked(self):
if Job.objects.filter(job_template=self, status__in=['pending', 'waiting', 'running']).count() > getattr(tower_settings, 'SCHEDULE_MAX_JOBS', 10):

View File

@ -352,6 +352,7 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin):
'host_config_key': settings.SYSTEM_UUID,
'created_by': created_by,
'playbook': playbook,
'ask_credential_on_launch': True,
}
opts.update(kwargs)
return JobTemplate.objects.create(**opts)

View File

@ -0,0 +1,295 @@
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
from copy import copy
@pytest.fixture
def runtime_data(organization):
cred_obj = Credential.objects.create(name='runtime-cred', kind='ssh', username='test_user2', password='pas4word2')
inv_obj = organization.inventories.create(name="runtime-inv")
return dict(
extra_vars='{"job_launch_var": 4}',
limit='test-servers',
job_type='check',
job_tags='provision',
skip_tags='restart',
inventory=inv_obj.pk,
credential=cred_obj.pk,
)
@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,
ask_credential_on_launch=on_off,
)
return rf
@pytest.fixture
def job_template_prompts_null(project):
return JobTemplate.objects.create(
job_type='run',
project=project,
inventory=None,
credential=None,
name='deploy-job-template',
ask_variables_on_launch=True,
ask_tags_on_launch=True,
ask_job_type_on_launch=True,
ask_inventory_on_launch=True,
ask_limit_on_launch=True,
ask_credential_on_launch=True,
)
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(False)
job_template_saved = copy(job_template)
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, user('admin', True))
assert response.status_code == 201
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_saved.limit
assert job_obj.job_type == job_template_saved.job_type
assert job_obj.inventory.pk == job_template_saved.inventory.pk
assert job_obj.job_tags == job_template_saved.job_tags
assert job_obj.credential.pk == job_template_saved.credential.pk
# 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 'credential' 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
@pytest.mark.job_runtime_vars
def test_job_accept_prompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True)
admin_user = user('admin', True)
job_template.inventory.execute_role.members.add(admin_user)
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, admin_user)
assert response.status_code == 201
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.credential.pk == runtime_data['credential']
assert job_obj.job_tags == runtime_data['job_tags']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null, post, user):
job_template = job_template_prompts_null
common_user = user('not-admin', False)
# Give user permission to execute the job template
job_template.execute_role.members.add(common_user)
# Give user permission to use inventory and credential at runtime
credential = Credential.objects.get(pk=runtime_data['credential'])
credential.use_role.members.add(common_user)
inventory = Inventory.objects.get(pk=runtime_data['inventory'])
inventory.use_role.members.add(common_user)
response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, common_user)
assert response.status_code == 201
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.credential.pk == runtime_data['credential']
assert job_obj.job_tags == runtime_data['job_tags']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_reject_invalid_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]),
dict(job_type='foobicate', # foobicate is not a valid job type
inventory=87865, credential=48474), user('admin', True))
assert response.status_code == 400
assert response.data['job_type'] == [u'"foobicate" is not a valid choice.']
assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.']
assert response.data['credential'] == [u'Invalid pk "48474" - object does not exist.']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_reject_invalid_prompted_extra_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]),
dict(extra_vars='{"unbalanced brackets":'), user('admin', True))
assert response.status_code == 400
assert response.data['extra_vars'] == ['Must be valid JSON or YAML']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_fails_without_inventory(deploy_jobtemplate, post, user):
deploy_jobtemplate.inventory = None
deploy_jobtemplate.save()
response = post(reverse('api:job_template_launch',
args=[deploy_jobtemplate.pk]), {}, user('admin', True))
assert response.status_code == 400
assert response.data['inventory'] == ['Job Template Inventory is missing or undefined']
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_fails_without_inventory_access(deploy_jobtemplate, machine_credential, post, user):
deploy_jobtemplate.ask_inventory_on_launch = True
deploy_jobtemplate.credential = machine_credential
deploy_jobtemplate.save()
common_user = user('test-user', False)
deploy_jobtemplate.execute_role.members.add(common_user)
deploy_jobtemplate.inventory.use_role.members.add(common_user)
deploy_jobtemplate.project.member_role.members.add(common_user)
deploy_jobtemplate.credential.use_role.members.add(common_user)
# Assure that the base job template can be launched to begin with
response = post(reverse('api:job_template_launch',
args=[deploy_jobtemplate.pk]), {}, common_user)
assert response.status_code == 201
# Assure that giving an inventory without access to the inventory blocks the launch
new_inv = deploy_jobtemplate.project.organization.inventories.create(name="user-can-not-use")
response = post(reverse('api:job_template_launch', args=[deploy_jobtemplate.pk]),
dict(inventory=new_inv.pk), common_user)
assert response.status_code == 403
assert response.data['detail'] == u'You do not have permission to perform this action.'
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_relaunch_prompted_vars(runtime_data, job_template_prompts, post, user):
job_template = job_template_prompts(True)
admin_user = user('admin', True)
# Launch job, overwriting several JT fields
first_response = post(reverse('api:job_template_launch', args=[job_template.pk]),
runtime_data, admin_user)
assert first_response.status_code == 201
original_job = Job.objects.get(pk=first_response.data['job'])
# Launch a second job as a relaunch of the first
second_response = post(reverse('api:job_relaunch', args=[original_job.pk]),
{}, admin_user)
relaunched_job = Job.objects.get(pk=second_response.data['job'])
# Check that job data matches the original runtime variables
assert first_response.status_code == 201
assert 'job_launch_var' in yaml.load(relaunched_job.extra_vars)
assert relaunched_job.limit == runtime_data['limit']
assert relaunched_job.job_type == runtime_data['job_type']
assert relaunched_job.inventory.pk == runtime_data['inventory']
assert relaunched_job.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.ask_credential_on_launch = True
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
assert job_obj.credential.id == machine_credential.id
@pytest.mark.django_db
@pytest.mark.job_runtime_vars
def test_job_launch_unprompted_vars_with_survey(mocker, job_template_prompts, post, user):
with mocker.patch('awx.main.access.BaseAccess.check_license', return_value=False):
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))
assert response.status_code == 201
job_id = response.data['job']
job_obj = Job.objects.get(pk=job_id)
# Check that the survey variable is accepted 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

View File

@ -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")

View File

@ -503,6 +503,7 @@ class BaseJobTestMixin(BaseTestMixin):
playbook=self.proj_dev.playbooks[0],
host_config_key=uuid.uuid4().hex,
created_by=self.user_sue,
ask_credential_on_launch=True,
)
# self.job_eng_run = self.jt_eng_run.create_job(
# created_by=self.user_sue,

View File

@ -191,32 +191,23 @@ class CreateDefaultOrgTest(BaseCommandMixin, BaseTest):
def setUp(self):
super(CreateDefaultOrgTest, self).setUp()
self.setup_instances()
def test_create_default_org(self):
self.setup_users()
self.assertEqual(Organization.objects.count(), 0)
result, stdout, stderr = self.run_command('create_default_org')
result, stdout, stderr = self.run_command('create_preload_data')
self.assertEqual(result, None)
self.assertTrue('Default organization added' in stdout)
self.assertEqual(Organization.objects.count(), 1)
org = Organization.objects.all()[0]
self.assertEqual(org.created_by, self.super_django_user)
self.assertEqual(org.modified_by, self.super_django_user)
result, stdout, stderr = self.run_command('create_default_org')
result, stdout, stderr = self.run_command('create_preload_data')
self.assertEqual(result, None)
self.assertFalse('Default organization added' in stdout)
self.assertEqual(Organization.objects.count(), 1)
def test_create_default_org_when_no_superuser_exists(self):
self.assertEqual(Organization.objects.count(), 0)
result, stdout, stderr = self.run_command('create_default_org')
self.assertEqual(result, None)
self.assertTrue('Default organization added' in stdout)
self.assertEqual(Organization.objects.count(), 1)
org = Organization.objects.all()[0]
self.assertEqual(org.created_by, None)
self.assertEqual(org.modified_by, None)
class DumpDataTest(BaseCommandMixin, BaseTest):
'''
Test cases for dumpdata management command.

View File

@ -27,6 +27,8 @@ 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,
ask_credential_on_launch = True,
)
self.data_no_cred = dict(
name = 'launched job template no credential',
@ -34,6 +36,8 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
inventory = self.inv_eng.pk,
project = self.proj_dev.pk,
playbook = self.proj_dev.playbooks[0],
ask_credential_on_launch = True,
ask_variables_on_launch = True,
)
self.data_cred_ask = dict(self.data)
self.data_cred_ask['name'] = 'launched job templated with ask passwords'
@ -67,7 +71,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
def test_credential_implicit(self):
# Implicit, attached credentials
with self.current_user(self.user_sue):
response = self.post(self.launch_url, {}, expect=202)
response = self.post(self.launch_url, {}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertTrue(j.status == 'new')
@ -75,7 +79,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Sending extra_vars as a JSON string, implicit credentials
with self.current_user(self.user_sue):
data = dict(extra_vars = '{\"a\":3}')
response = self.post(self.launch_url, data, expect=202)
response = self.post(self.launch_url, data, expect=201)
j = Job.objects.get(pk=response['job'])
ev_dict = yaml.load(j.extra_vars)
self.assertIn('a', ev_dict)
@ -86,7 +90,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Sending extra_vars as a JSON string, implicit credentials
with self.current_user(self.user_sue):
data = dict(extra_vars = 'a: 3')
response = self.post(self.launch_url, data, expect=202)
response = self.post(self.launch_url, data, expect=201)
j = Job.objects.get(pk=response['job'])
ev_dict = yaml.load(j.extra_vars)
self.assertIn('a', ev_dict)
@ -97,7 +101,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Explicit, credential
with self.current_user(self.user_sue):
self.cred_sue.delete()
response = self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=202)
response = self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertEqual(j.status, 'new')
self.assertEqual(j.credential.pk, self.cred_doug.pk)
@ -106,7 +110,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Explicit, credential
with self.current_user(self.user_sue):
self.cred_sue.delete()
response = self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=202)
response = self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertEqual(j.status, 'new')
self.assertEqual(j.credential.pk, self.cred_doug.pk)
@ -114,7 +118,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
def test_credential_override(self):
# Explicit, credential
with self.current_user(self.user_sue):
response = self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=202)
response = self.post(self.launch_url, {'credential': self.cred_doug.pk}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertEqual(j.status, 'new')
self.assertEqual(j.credential.pk, self.cred_doug.pk)
@ -122,7 +126,7 @@ class JobTemplateLaunchTest(BaseJobTestMixin, django.test.TransactionTestCase):
def test_credential_override_via_credential_id(self):
# Explicit, credential
with self.current_user(self.user_sue):
response = self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=202)
response = self.post(self.launch_url, {'credential_id': self.cred_doug.pk}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertEqual(j.status, 'new')
self.assertEqual(j.credential.pk, self.cred_doug.pk)
@ -191,6 +195,7 @@ class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TransactionTe
project = self.proj_dev.pk,
credential = self.cred_sue_ask.pk,
playbook = self.proj_dev.playbooks[0],
ask_credential_on_launch = True,
)
with self.current_user(self.user_sue):
@ -211,7 +216,7 @@ class JobTemplateLaunchPasswordsTest(BaseJobTestMixin, django.test.TransactionTe
def test_explicit_cred_with_ask_password(self):
with self.current_user(self.user_sue):
response = self.post(self.launch_url, {'ssh_password': 'whatever'}, expect=202)
response = self.post(self.launch_url, {'ssh_password': 'whatever'}, expect=201)
j = Job.objects.get(pk=response['job'])
self.assertEqual(j.status, 'new')

View File

@ -798,7 +798,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertEqual(jobs_qs.count(), 0)
# Create the job itself.
result = self.post(url, data, expect=202, remote_addr=host_ip)
result = self.post(url, data, expect=201, remote_addr=host_ip)
# Establish that we got back what we expect, and made the changes
# that we expect.
@ -813,7 +813,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertEqual(job.hosts.all()[0], host)
# Create the job itself using URL-encoded form data instead of JSON.
result = self.post(url, data, expect=202, remote_addr=host_ip, data_type='form')
result = self.post(url, data, expect=201, remote_addr=host_ip, data_type='form')
# Establish that we got back what we expect, and made the changes
# that we expect.
@ -829,7 +829,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Run the callback job again with extra vars and verify their presence
data.update(dict(extra_vars=dict(key="value")))
result = self.post(url, data, expect=202, remote_addr=host_ip)
result = self.post(url, data, expect=201, remote_addr=host_ip)
jobs_qs = job_template.jobs.filter(launch_type='callback').order_by('-pk')
job = jobs_qs[0]
self.assertTrue("key" in job.extra_vars)
@ -878,7 +878,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
break
self.assertTrue(host)
self.assertEqual(jobs_qs.count(), 3)
self.post(url, data, expect=202, remote_addr=host_ip)
self.post(url, data, expect=201, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 4)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
@ -903,7 +903,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
break
self.assertTrue(host)
self.assertEqual(jobs_qs.count(), 4)
self.post(url, data, expect=202, remote_addr=host_ip)
self.post(url, data, expect=201, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 5)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
@ -917,7 +917,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
host = host_qs[0]
host_ip = host.variables_dict['ansible_ssh_host']
self.assertEqual(jobs_qs.count(), 5)
self.post(url, data, expect=202, remote_addr=host_ip)
self.post(url, data, expect=201, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 6)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
@ -951,7 +951,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
break
self.assertTrue(host)
self.assertEqual(jobs_qs.count(), 6)
self.post(url, data, expect=202, remote_addr=host_ip)
self.post(url, data, expect=201, remote_addr=host_ip)
self.assertEqual(jobs_qs.count(), 7)
job = jobs_qs[0]
self.assertEqual(job.launch_type, 'callback')
@ -1087,7 +1087,7 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase):
response = self.get(url)
self.assertTrue(response['can_start'])
self.assertFalse(response['passwords_needed_to_start'])
response = self.post(url, {}, expect=202)
response = self.post(url, {}, expect=201)
job = Job.objects.get(pk=job.pk)
self.assertEqual(job.status, 'successful', job.result_stdout)
self.assertFalse(errors)
@ -1146,14 +1146,14 @@ class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
# should return, and should be able to launch template without error.
response = self.get(launch_url)
self.assertFalse(response['survey_enabled'])
self.post(launch_url, {}, expect=202)
self.post(launch_url, {}, expect=201)
# Now post a survey spec and check that the answer is set in the
# job's extra vars.
self.post(url, json.loads(TEST_SIMPLE_REQUIRED_SURVEY), expect=200)
response = self.get(launch_url)
self.assertTrue(response['survey_enabled'])
self.assertTrue('favorite_color' in response['variables_needed_to_start'])
response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=202)
response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=201)
job = Job.objects.get(pk=response["job"])
job_extra = json.loads(job.extra_vars)
self.assertTrue("favorite_color" in job_extra)
@ -1187,7 +1187,7 @@ class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
with self.current_user(self.user_sue):
response = self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
# Just the required answer should work
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=202)
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=201)
# Short answer but requires a long answer
self.post(launch_url, dict(extra_vars=dict(long_answer='a', reqd_answer="foo")), expect=400)
# Long answer but requires a short answer
@ -1199,9 +1199,9 @@ class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Integer that's too big
self.post(launch_url, dict(extra_vars=dict(int_answer=10, reqd_answer="foo")), expect=400)
# Integer that's just riiiiight
self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=202)
self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=201)
# Integer bigger than min with no max defined
self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=202)
self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=201)
# Integer answer that's the wrong type
self.post(launch_url, dict(extra_vars=dict(int_answer="test", reqd_answer="foo")), expect=400)
# Float that's too big
@ -1209,7 +1209,7 @@ class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Float that's too small
self.post(launch_url, dict(extra_vars=dict(float_answer=1.995, reqd_answer="foo")), expect=400)
# float that's just riiiiight
self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=202)
self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=201)
# float answer that's the wrong type
self.post(launch_url, dict(extra_vars=dict(float_answer="test", reqd_answer="foo")), expect=400)
# Wrong choice in single choice
@ -1219,11 +1219,11 @@ class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TransactionTestCase):
# Wrong type for multi choicen
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice="two")), expect=400)
# Right choice in single choice
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=202)
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=201)
# Right choices in multi choice
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=202)
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=201)
# Nested json
self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202)
self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=201)
# Bob can access and update the survey because he's an org-admin
with self.current_user(self.user_bob):