More job template tests, enable creating a new job by posting to the job template job list.

This commit is contained in:
Chris Church
2013-05-11 05:11:31 -04:00
parent 6e6600b822
commit a20573a68d
7 changed files with 206 additions and 66 deletions

View File

@@ -511,7 +511,7 @@ class JobTemplateAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
''' '''
''' '''
return False # FIXME return self.user.is_superuser # FIXME
class JobAccess(BaseAccess): class JobAccess(BaseAccess):

View File

@@ -16,6 +16,7 @@
import os import os
import shlex
from django.conf import settings from django.conf import settings
from django.db import models, DatabaseError from django.db import models, DatabaseError
from django.db.models import CASCADE, SET_NULL, PROTECT from django.db.models import CASCADE, SET_NULL, PROTECT
@@ -25,7 +26,6 @@ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.timezone import now from django.utils.timezone import now
import exceptions
from jsonfield import JSONField from jsonfield import JSONField
from djcelery.models import TaskMeta from djcelery.models import TaskMeta
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
@@ -679,6 +679,13 @@ class Job(CommonModel):
@property @property
def extra_vars_dict(self): def extra_vars_dict(self):
'''Return extra_vars key=value pairs as a dictionary.''' '''Return extra_vars key=value pairs as a dictionary.'''
d = {}
extra_vars = self.extra_vars.encode('utf-8')
for kv in [x.decode('utf-8') for x in shlex.split(extra_vars, posix=True)]:
if '=' in kv:
k, v = kv.split('=', 1)
d[k] = v
return d
@property @property
def celery_task(self): def celery_task(self):

View File

@@ -376,6 +376,13 @@ class JobTemplateSerializer(BaseSerializer):
res['credential'] = reverse('main:credentials_detail', args=(obj.credential.pk,)) res['credential'] = reverse('main:credentials_detail', args=(obj.credential.pk,))
return res return res
def validate_playbook(self, attrs, source):
project = attrs.get('project', None)
playbook = attrs.get('playbook', '')
if project and playbook and playbook not in project.playbooks:
raise serializers.ValidationError('Playbook not found for project')
return attrs
class JobSerializer(BaseSerializer): class JobSerializer(BaseSerializer):
passwords_needed_to_start = serializers.Field(source='get_passwords_needed_to_start') passwords_needed_to_start = serializers.Field(source='get_passwords_needed_to_start')
@@ -402,6 +409,28 @@ class JobSerializer(BaseSerializer):
res['job_template'] = reverse('main:job_template_detail', args=(obj.job_template.pk,)) res['job_template'] = reverse('main:job_template_detail', args=(obj.job_template.pk,))
return res return res
def from_native(self, data, files):
# When creating a new job and a job template is specified, populate any
# fields not provided in data from the job template.
if not self.object and isinstance(data, dict) and 'job_template' in data:
try:
job_template = JobTemplate.objects.get(pk=data['job_template'])
except JobTemplate.DoesNotExist:
self._errors = {'job_template': 'Invalid job template'}
return
# Don't auto-populate name or description.
data.setdefault('job_type', job_template.job_type)
data.setdefault('inventory', job_template.inventory.pk)
data.setdefault('project', job_template.project.pk)
data.setdefault('playbook', job_template.playbook)
if job_template.credential:
data.setdefault('credential', job_template.credential.pk)
data.setdefault('forks', job_template.forks)
data.setdefault('limit', job_template.limit)
data.setdefault('verbosity', job_template.verbosity)
data.setdefault('extra_vars', job_template.extra_vars)
return super(JobSerializer, self).from_native(data, files)
class JobHostSummarySerializer(BaseSerializer): class JobHostSummarySerializer(BaseSerializer):
class Meta: class Meta:

View File

@@ -55,7 +55,10 @@ class BaseTestMixin(object):
username = user_or_username username = user_or_username
password = password or self._user_passwords.get(username) password = password or self._user_passwords.get(username)
previous_auth = self._current_auth previous_auth = self._current_auth
self._current_auth = (username, password) if username is None:
self._current_auth = None
else:
self._current_auth = (username, password)
yield yield
finally: finally:
self._current_auth = previous_auth self._current_auth = previous_auth
@@ -179,8 +182,8 @@ class BaseTestMixin(object):
assert response.status_code == expect, "expected status %s, got %s for url=%s as auth=%s: %s" % (expect, response.status_code, url, auth, response.content) assert response.status_code == expect, "expected status %s, got %s for url=%s as auth=%s: %s" % (expect, response.status_code, url, auth, response.content)
if method_name == 'head': if method_name == 'head':
self.assertFalse(response.content) self.assertFalse(response.content)
if response.status_code not in [ 202, 204, 400, 405, 409 ] and method_name != 'head': if response.status_code not in [ 202, 204, 405, 409 ] and method_name != 'head' and response.content:
# no JSON responses in these at least for now, 400/409 should probably return some (FIXME) # no JSON responses in these at least for now, 409 should probably return some (FIXME)
return json.loads(response.content) return json.loads(response.content)
else: else:
return None return None

View File

@@ -317,7 +317,11 @@ class BaseJobTest(BaseTest):
project=self.proj_dev, project=self.proj_dev,
playbook=self.proj_dev.playbooks[0], playbook=self.proj_dev.playbooks[0],
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_eng_check = self.jt_eng_check.create_job(
created_by=self.user_sue,
credential=self.cred_doug,
)
self.jt_eng_run = JobTemplate.objects.create( self.jt_eng_run = JobTemplate.objects.create(
name='eng-dev-run', name='eng-dev-run',
job_type='run', job_type='run',
@@ -326,6 +330,10 @@ class BaseJobTest(BaseTest):
playbook=self.proj_dev.playbooks[0], playbook=self.proj_dev.playbooks[0],
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_eng_run = self.jt_eng_run.create_job(
created_by=self.user_sue,
credential=self.cred_chuck,
)
# Support has job templates to check/run the test project onto # Support has job templates to check/run the test project onto
# their own inventory. # their own inventory.
@@ -336,7 +344,11 @@ class BaseJobTest(BaseTest):
project=self.proj_test, project=self.proj_test,
playbook=self.proj_test.playbooks[0], playbook=self.proj_test.playbooks[0],
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_sup_check = self.jt_sup_check.create_job(
created_by=self.user_sue,
credential=self.cred_frank,
)
self.jt_sup_run = JobTemplate.objects.create( self.jt_sup_run = JobTemplate.objects.create(
name='sup-test-run', name='sup-test-run',
job_type='run', job_type='run',
@@ -345,6 +357,10 @@ class BaseJobTest(BaseTest):
playbook=self.proj_test.playbooks[0], playbook=self.proj_test.playbooks[0],
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_sup_run = self.jt_sup_run.create_job(
created_by=self.user_sue,
credential=self.cred_eve,
)
# Operations has job templates to check/run the prod project onto # Operations has job templates to check/run the prod project onto
# both east and west inventories, by default using the team credential. # both east and west inventories, by default using the team credential.
@@ -356,7 +372,10 @@ class BaseJobTest(BaseTest):
playbook=self.proj_prod.playbooks[0], playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_east, credential=self.cred_ops_east,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_ops_east_check = self.jt_ops_east_check.create_job(
created_by=self.user_sue,
)
self.jt_ops_east_run = JobTemplate.objects.create( self.jt_ops_east_run = JobTemplate.objects.create(
name='ops-east-prod-run', name='ops-east-prod-run',
job_type='run', job_type='run',
@@ -366,6 +385,9 @@ class BaseJobTest(BaseTest):
credential=self.cred_ops_east, credential=self.cred_ops_east,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_ops_east_run = self.jt_ops_east_run.create_job(
created_by=self.user_sue,
)
self.jt_ops_west_check = JobTemplate.objects.create( self.jt_ops_west_check = JobTemplate.objects.create(
name='ops-west-prod-check', name='ops-west-prod-check',
job_type='check', job_type='check',
@@ -374,7 +396,10 @@ class BaseJobTest(BaseTest):
playbook=self.proj_prod.playbooks[0], playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_west, credential=self.cred_ops_west,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_ops_west_check = self.jt_ops_west_check.create_job(
created_by=self.user_sue,
)
self.jt_ops_west_run = JobTemplate.objects.create( self.jt_ops_west_run = JobTemplate.objects.create(
name='ops-west-prod-run', name='ops-west-prod-run',
job_type='run', job_type='run',
@@ -384,6 +409,9 @@ class BaseJobTest(BaseTest):
credential=self.cred_ops_west, credential=self.cred_ops_west,
created_by=self.user_sue, created_by=self.user_sue,
) )
self.job_ops_west_run = self.jt_ops_west_run.create_job(
created_by=self.user_sue,
)
def setUp(self): def setUp(self):
super(BaseJobTest, self).setUp() super(BaseJobTest, self).setUp()
@@ -395,19 +423,23 @@ class JobTemplateTest(BaseJobTest):
def setUp(self): def setUp(self):
super(JobTemplateTest, self).setUp() super(JobTemplateTest, self).setUp()
def _test_invalid_creds(self, url, data=None, methods=None):
data = data or {}
methods = methods or ('options', 'head', 'get')
for auth in [(None,), ('invalid', 'password')]:
with self.current_user(*auth):
for method in methods:
f = getattr(self, method)
if method in ('post', 'put'):
f(url, data, expect=401)
else:
f(url, expect=401)
def test_get_job_template_list(self): def test_get_job_template_list(self):
url = reverse('main:job_template_list') url = reverse('main:job_template_list')
# no credentials == 401 # Test with no auth and with invalid login.
self.options(url, expect=401) self._test_invalid_creds(url)
self.head(url, expect=401)
self.get(url, expect=401)
# wrong credentials == 401
with self.current_user('invalid', 'password'):
self.options(url, expect=401)
self.head(url, expect=401)
self.get(url, expect=401)
# sue's credentials (superuser) == 200, full list # sue's credentials (superuser) == 200, full list
with self.current_user(self.user_sue): with self.current_user(self.user_sue):
@@ -418,7 +450,7 @@ class JobTemplateTest(BaseJobTest):
self.check_pagination_and_size(response, qs.count()) self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs) self.check_list_ids(response, qs)
# FIXME: Check individual job template result. # FIXME: Check individual job template result fields.
# alex's credentials (admin of all orgs) == 200, full list # alex's credentials (admin of all orgs) == 200, full list
with self.current_user(self.user_alex): with self.current_user(self.user_alex):
@@ -441,80 +473,132 @@ class JobTemplateTest(BaseJobTest):
#self.check_pagination_and_size(response, qs.count()) #self.check_pagination_and_size(response, qs.count())
#self.check_list_ids(response, qs) #self.check_list_ids(response, qs)
# FIXME: Check with other credentials.
def test_post_job_template_list(self): def test_post_job_template_list(self):
url = reverse('main:job_template_list') url = reverse('main:job_template_list')
return # FIXME
# org admin can add job template
data = dict( data = dict(
name = 'job-foo', name = 'new job template',
credential = self.user_credential.pk,
inventory = self.inventory.pk,
project = self.project.pk,
job_type = PERM_INVENTORY_DEPLOY, job_type = PERM_INVENTORY_DEPLOY,
playbook = self.project.playbooks[0], inventory = self.inv_eng.pk,
project = self.proj_dev.pk,
playbook = self.proj_dev.playbooks[0],
) )
with self.current_user(self.normal_django_user):
# Test with no auth and with invalid login.
self._test_invalid_creds(url, data, methods=('post',))
# sue can always add job templates.
with self.current_user(self.user_sue):
response = self.post(url, data, expect=201) response = self.post(url, data, expect=201)
detail_url = reverse('main:job_template_detail', detail_url = reverse('main:job_template_detail',
args=(response['id'],)) args=(response['id'],))
self.assertEquals(response['url'], detail_url) self.assertEquals(response['url'], detail_url)
# other_django_user is on a team that can deploy, so can create both # Check that all fields provided were set.
# deploy and check type job templates jt = JobTemplate.objects.get(pk=response['id'])
with self.current_user(self.other_django_user): self.assertEqual(jt.name, data['name'])
data['name'] = 'job-foo2' self.assertEqual(jt.job_type, data['job_type'])
response = self.post(url, data, expect=201) self.assertEqual(jt.inventory.pk, data['inventory'])
data['name'] = 'job-foo3' self.assertEqual(jt.credential, None)
data['job_type'] = PERM_INVENTORY_CHECK self.assertEqual(jt.project.pk, data['project'])
response = self.post(url, data, expect=201) self.assertEqual(jt.playbook, data['playbook'])
# other2_django_user has individual permissions to run check mode, # Test that all required fields are really required.
# but not deploy data['name'] = 'another new job template'
with self.current_user(self.other2_django_user): for field in ('name', 'job_type', 'inventory', 'project', 'playbook'):
data['name'] = 'job-foo4' with self.current_user(self.user_sue):
#data['credential'] = self.user_credential.pk d = dict(data.items())
#response = self.post(url, data, expect=201) d.pop(field)
data['name'] = 'job-foo5' response = self.post(url, d, expect=400)
data['job_type'] = PERM_INVENTORY_DEPLOY self.assertTrue(field in response,
response = self.post(url, data, expect=403) 'no error for field "%s" in response' % field)
# nobody user can't even run check mode # Test invalid value for job_type.
with self.current_user(self.nobody_django_user): with self.current_user(self.user_sue):
data['name'] = 'job-foo5' d = dict(data.items())
data['job_type'] = PERM_INVENTORY_CHECK d['job_type'] = 'world domination'
response = self.post(url, data, expect=403) response = self.post(url, d, expect=400)
data['job_type'] = PERM_INVENTORY_DEPLOY self.assertTrue('job_type' in response)
response = self.post(url, data, expect=403)
# Test playbook not in list of project playbooks.
with self.current_user(self.user_sue):
d = dict(data.items())
d['playbook'] = 'no_playbook_here.yml'
response = self.post(url, d, expect=400)
self.assertTrue('playbook' in response)
# FIXME: Check other credentials and optional fields.
def test_get_job_template_detail(self): def test_get_job_template_detail(self):
jt = self.jt_eng_run
return # FIXME url = reverse('main:job_template_detail', args=(jt.pk,))
url = reverse('main:job_template_detail', args=(self.job_template1.pk,)) # Test with no auth and with invalid login.
self._test_invalid_creds(url)
# verify we can also get the job template record
with self.current_user(self.other2_django_user): # sue can read the job template detail.
with self.current_user(self.user_sue):
self.options(url) self.options(url)
self.head(url) self.head(url)
response = self.get(url) response = self.get(url)
self.assertEqual(response['url'], url) self.assertEqual(response['url'], url)
# FIXME: Check other credentials and optional fields.
# TODO: add more tests that show # TODO: add more tests that show
# the method used to START a JobTemplate follow the exact same permissions as those to create it ... # the method used to START a JobTemplate follow the exact same permissions as those to create it ...
# and that jobs come back nicely serialized with related resources and so on ... # and that jobs come back nicely serialized with related resources and so on ...
# that we can drill all the way down and can get at host failure lists, etc ... # that we can drill all the way down and can get at host failure lists, etc ...
def test_put_job_template_detail(self): def test_put_job_template_detail(self):
pass jt = self.jt_eng_run
url = reverse('main:job_template_detail', args=(jt.pk,))
# Test with no auth and with invalid login.
self._test_invalid_creds(url, methods=('put',))
# sue can update the job template detail.
with self.current_user(self.user_sue):
data = self.get(url)
response = self.put(url, data)
# FIXME: Check other credentials and optional fields.
def test_get_job_template_job_list(self): def test_get_job_template_job_list(self):
pass jt = self.jt_eng_run
url = reverse('main:job_template_job_list', args=(jt.pk,))
# Test with no auth and with invalid login.
self._test_invalid_creds(url)
# sue can read the job template job list.
with self.current_user(self.user_sue):
self.options(url)
self.head(url)
response = self.get(url)
qs = jt.jobs.all()
self.check_pagination_and_size(response, qs.count())
self.check_list_ids(response, qs)
# FIXME: Check other credentials and optional fields.
def test_post_job_template_job_list(self): def test_post_job_template_job_list(self):
pass jt = self.jt_eng_run
url = reverse('main:job_template_job_list', args=(jt.pk,))
data = dict(
name='new job from template',
credential=self.cred_bob.pk,
)
# Test with no auth and with invalid login.
self._test_invalid_creds(url, data, methods=('post',))
# sue can create a new job from the template.
with self.current_user(self.user_sue):
response = self.post(url, data, expect=201)
# FIXME: Check other credentials and optional fields.
class JobTest(BaseJobTest): class JobTest(BaseJobTest):
@@ -533,7 +617,16 @@ class JobTest(BaseJobTest):
def test_put_job_detail(self): def test_put_job_detail(self):
pass pass
def test_post_job_detail(self): def test_get_job_start(self):
pass
def test_post_job_start(self):
pass
def test_get_job_cancel(self):
pass
def test_post_job_cancel(self):
pass pass
def test_get_job_host_list(self): def test_get_job_host_list(self):

View File

@@ -116,6 +116,8 @@ job_templates_urls = patterns('lib.main.views',
jobs_urls = patterns('lib.main.views', jobs_urls = patterns('lib.main.views',
url(r'^$', 'job_list'), url(r'^$', 'job_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_detail'), url(r'^(?P<pk>[0-9]+)/$', 'job_detail'),
url(r'^(?P<pk>[0-9]+)/start/$', 'job_start'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'job_cancel'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_hosts_list'), url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_hosts_list'),
url(r'^(?P<pk>[0-9]+)/successful_hosts/$', 'jobs_successful_hosts_list'), url(r'^(?P<pk>[0-9]+)/successful_hosts/$', 'jobs_successful_hosts_list'),
url(r'^(?P<pk>[0-9]+)/changed_hosts/$', 'jobs_changed_hosts_list'), url(r'^(?P<pk>[0-9]+)/changed_hosts/$', 'jobs_changed_hosts_list'),

View File

@@ -913,7 +913,6 @@ class JobTemplateList(BaseList):
def _get_queryset(self): def _get_queryset(self):
return get_user_queryset(self.request.user, self.model) return get_user_queryset(self.request.user, self.model)
class JobTemplateDetail(BaseDetail): class JobTemplateDetail(BaseDetail):
model = JobTemplate model = JobTemplate
@@ -955,6 +954,13 @@ class JobDetail(BaseDetail):
serializer_class = JobSerializer serializer_class = JobSerializer
permission_classes = (CustomRbac,) permission_classes = (CustomRbac,)
class JobStart(BaseDetail):
def post(self, request, *args, **kwargs):
pass # FIXME
class JobCancel(BaseDetail):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
pass # FIXME pass # FIXME