From a20573a68d117fc41cd48ae625c5dc71b8e42450 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Sat, 11 May 2013 05:11:31 -0400 Subject: [PATCH] More job template tests, enable creating a new job by posting to the job template job list. --- lib/main/access.py | 2 +- lib/main/models/__init__.py | 9 +- lib/main/serializers.py | 29 +++++ lib/main/tests/base.py | 9 +- lib/main/tests/jobs.py | 213 ++++++++++++++++++++++++++---------- lib/main/urls.py | 2 + lib/main/views.py | 8 +- 7 files changed, 206 insertions(+), 66 deletions(-) diff --git a/lib/main/access.py b/lib/main/access.py index 450dfcdfc6..97c334b7f9 100644 --- a/lib/main/access.py +++ b/lib/main/access.py @@ -511,7 +511,7 @@ class JobTemplateAccess(BaseAccess): def can_change(self, obj, data): ''' ''' - return False # FIXME + return self.user.is_superuser # FIXME class JobAccess(BaseAccess): diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 0b8f9464f6..986b9ce50c 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -16,6 +16,7 @@ import os +import shlex from django.conf import settings from django.db import models, DatabaseError 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.contrib.auth.models import User from django.utils.timezone import now -import exceptions from jsonfield import JSONField from djcelery.models import TaskMeta from rest_framework.authtoken.models import Token @@ -679,6 +679,13 @@ class Job(CommonModel): @property def extra_vars_dict(self): '''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 def celery_task(self): diff --git a/lib/main/serializers.py b/lib/main/serializers.py index 6fe7291c8d..2667e5fb3f 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -376,6 +376,13 @@ class JobTemplateSerializer(BaseSerializer): res['credential'] = reverse('main:credentials_detail', args=(obj.credential.pk,)) 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): 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,)) 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 Meta: diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py index a783a490f6..4bcf2f9e5c 100644 --- a/lib/main/tests/base.py +++ b/lib/main/tests/base.py @@ -55,7 +55,10 @@ class BaseTestMixin(object): username = user_or_username password = password or self._user_passwords.get(username) 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 finally: 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) if method_name == 'head': self.assertFalse(response.content) - if response.status_code not in [ 202, 204, 400, 405, 409 ] and method_name != 'head': - # no JSON responses in these at least for now, 400/409 should probably return some (FIXME) + 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, 409 should probably return some (FIXME) return json.loads(response.content) else: return None diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py index 3f84459484..4b98013710 100644 --- a/lib/main/tests/jobs.py +++ b/lib/main/tests/jobs.py @@ -317,7 +317,11 @@ class BaseJobTest(BaseTest): project=self.proj_dev, playbook=self.proj_dev.playbooks[0], 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( name='eng-dev-run', job_type='run', @@ -326,6 +330,10 @@ class BaseJobTest(BaseTest): playbook=self.proj_dev.playbooks[0], 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 # their own inventory. @@ -336,7 +344,11 @@ class BaseJobTest(BaseTest): project=self.proj_test, playbook=self.proj_test.playbooks[0], 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( name='sup-test-run', job_type='run', @@ -345,6 +357,10 @@ class BaseJobTest(BaseTest): playbook=self.proj_test.playbooks[0], 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 # both east and west inventories, by default using the team credential. @@ -356,7 +372,10 @@ class BaseJobTest(BaseTest): playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_east, 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( name='ops-east-prod-run', job_type='run', @@ -366,6 +385,9 @@ class BaseJobTest(BaseTest): credential=self.cred_ops_east, 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( name='ops-west-prod-check', job_type='check', @@ -374,7 +396,10 @@ class BaseJobTest(BaseTest): playbook=self.proj_prod.playbooks[0], credential=self.cred_ops_west, 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( name='ops-west-prod-run', job_type='run', @@ -384,6 +409,9 @@ class BaseJobTest(BaseTest): credential=self.cred_ops_west, 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): super(BaseJobTest, self).setUp() @@ -395,19 +423,23 @@ class JobTemplateTest(BaseJobTest): def setUp(self): 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): url = reverse('main:job_template_list') - # no credentials == 401 - self.options(url, expect=401) - 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) + # Test with no auth and with invalid login. + self._test_invalid_creds(url) # sue's credentials (superuser) == 200, full list with self.current_user(self.user_sue): @@ -418,7 +450,7 @@ class JobTemplateTest(BaseJobTest): self.check_pagination_and_size(response, qs.count()) 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 with self.current_user(self.user_alex): @@ -441,80 +473,132 @@ class JobTemplateTest(BaseJobTest): #self.check_pagination_and_size(response, qs.count()) #self.check_list_ids(response, qs) + # FIXME: Check with other credentials. def test_post_job_template_list(self): url = reverse('main:job_template_list') - - return # FIXME - - # org admin can add job template data = dict( - name = 'job-foo', - credential = self.user_credential.pk, - inventory = self.inventory.pk, - project = self.project.pk, + name = 'new job template', 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) detail_url = reverse('main:job_template_detail', args=(response['id'],)) self.assertEquals(response['url'], detail_url) - # other_django_user is on a team that can deploy, so can create both - # deploy and check type job templates - with self.current_user(self.other_django_user): - data['name'] = 'job-foo2' - response = self.post(url, data, expect=201) - data['name'] = 'job-foo3' - data['job_type'] = PERM_INVENTORY_CHECK - response = self.post(url, data, expect=201) + # Check that all fields provided were set. + jt = JobTemplate.objects.get(pk=response['id']) + self.assertEqual(jt.name, data['name']) + self.assertEqual(jt.job_type, data['job_type']) + self.assertEqual(jt.inventory.pk, data['inventory']) + self.assertEqual(jt.credential, None) + self.assertEqual(jt.project.pk, data['project']) + self.assertEqual(jt.playbook, data['playbook']) - # other2_django_user has individual permissions to run check mode, - # but not deploy - with self.current_user(self.other2_django_user): - data['name'] = 'job-foo4' - #data['credential'] = self.user_credential.pk - #response = self.post(url, data, expect=201) - data['name'] = 'job-foo5' - data['job_type'] = PERM_INVENTORY_DEPLOY - response = self.post(url, data, expect=403) + # Test that all required fields are really required. + data['name'] = 'another new job template' + for field in ('name', 'job_type', 'inventory', 'project', 'playbook'): + with self.current_user(self.user_sue): + d = dict(data.items()) + d.pop(field) + response = self.post(url, d, expect=400) + self.assertTrue(field in response, + 'no error for field "%s" in response' % field) - # nobody user can't even run check mode - with self.current_user(self.nobody_django_user): - data['name'] = 'job-foo5' - data['job_type'] = PERM_INVENTORY_CHECK - response = self.post(url, data, expect=403) - data['job_type'] = PERM_INVENTORY_DEPLOY - response = self.post(url, data, expect=403) + # Test invalid value for job_type. + with self.current_user(self.user_sue): + d = dict(data.items()) + d['job_type'] = 'world domination' + response = self.post(url, d, expect=400) + self.assertTrue('job_type' in response) + + # 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): - - return # FIXME - - url = reverse('main:job_template_detail', args=(self.job_template1.pk,)) - - # verify we can also get the job template record - with self.current_user(self.other2_django_user): + 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) + + # sue can read the job template detail. + with self.current_user(self.user_sue): self.options(url) self.head(url) response = self.get(url) self.assertEqual(response['url'], url) + # FIXME: Check other credentials and optional fields. + # TODO: add more tests that show # 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 ... # that we can drill all the way down and can get at host failure lists, etc ... 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): - 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): - 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): @@ -533,7 +617,16 @@ class JobTest(BaseJobTest): def test_put_job_detail(self): 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 def test_get_job_host_list(self): diff --git a/lib/main/urls.py b/lib/main/urls.py index 0008d979f7..a4dce23702 100644 --- a/lib/main/urls.py +++ b/lib/main/urls.py @@ -116,6 +116,8 @@ job_templates_urls = patterns('lib.main.views', jobs_urls = patterns('lib.main.views', url(r'^$', 'job_list'), url(r'^(?P[0-9]+)/$', 'job_detail'), + url(r'^(?P[0-9]+)/start/$', 'job_start'), + url(r'^(?P[0-9]+)/cancel/$', 'job_cancel'), url(r'^(?P[0-9]+)/hosts/$', 'job_hosts_list'), url(r'^(?P[0-9]+)/successful_hosts/$', 'jobs_successful_hosts_list'), url(r'^(?P[0-9]+)/changed_hosts/$', 'jobs_changed_hosts_list'), diff --git a/lib/main/views.py b/lib/main/views.py index c56467da80..5a1561e14f 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -913,7 +913,6 @@ class JobTemplateList(BaseList): def _get_queryset(self): return get_user_queryset(self.request.user, self.model) - class JobTemplateDetail(BaseDetail): model = JobTemplate @@ -955,6 +954,13 @@ class JobDetail(BaseDetail): serializer_class = JobSerializer permission_classes = (CustomRbac,) +class JobStart(BaseDetail): + + def post(self, request, *args, **kwargs): + pass # FIXME + +class JobCancel(BaseDetail): + def post(self, request, *args, **kwargs): pass # FIXME