From 58b71ca9f9b2ef16467283829101e500cf0e9208 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 4 Sep 2014 13:25:21 -0400 Subject: [PATCH] Implement /launch endpoint on job template Partial implementation of: https://trello.com/c/7uXHs7ze/14-require-admin-to-be-able-to-run-a-job-without-a-job-template --- awx/api/urls.py | 1 + awx/api/views.py | 25 +++++++++++++++++++++++++ awx/main/access.py | 20 ++++++++++++++++++++ awx/main/models/jobs.py | 12 ++++++++++++ awx/main/tests/base.py | 2 +- awx/main/tests/jobs.py | 26 ++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) diff --git a/awx/api/urls.py b/awx/api/urls.py index 05042660af..2d4770d63f 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -130,6 +130,7 @@ permission_urls = patterns('awx.api.views', job_template_urls = patterns('awx.api.views', url(r'^$', 'job_template_list'), url(r'^(?P[0-9]+)/$', 'job_template_detail'), + url(r'^(?P[0-9]+)launch/$', 'job_template_launch'), url(r'^(?P[0-9]+)/jobs/$', 'job_template_jobs_list'), url(r'^(?P[0-9]+)/callback/$', 'job_template_callback'), url(r'^(?P[0-9]+)/schedules/$', 'job_template_schedules_list'), diff --git a/awx/api/views.py b/awx/api/views.py index 92e817ed01..f1a4d810e9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1337,6 +1337,31 @@ class JobTemplateDetail(RetrieveUpdateDestroyAPIView): model = JobTemplate serializer_class = JobTemplateSerializer +class JobTemplateLaunch(GenericAPIView): + + model = JobTemplate + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = {} + data['can_start_without_user_input'] = obj.can_start_without_user_input() + data['passwords_needed_to_start'] = obj.passwords_needed_to_start + data['ask_variables_on_launch'] = obj.ask_variables_on_launch + return Response(data) + + def post(self, request, *args, **obj): + obj = self.get_object() + if not request.user.can_access(self.model, 'start', obj): + raise PermissionDenied() + new_job = obj.create_unified_job() + result = new_job.signal_start(**request.DATA) + if not result: + data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + data = dict(job=new_job.id) + return Response(data, status=status.HTTP_202_ACCEPTED) + class JobTemplateSchedulesList(SubListCreateAPIView): view_name = "Job Template Schedules" diff --git a/awx/main/access.py b/awx/main/access.py index 42c3cad936..942450aa7a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -920,6 +920,26 @@ class JobTemplateAccess(BaseAccess): return True + def can_start(self, obj): + reader = TaskSerializer() + validation_info = reader.from_file() + + if 'test' in sys.argv or 'jenkins' in sys.argv: + validation_info['free_instances'] = 99999999 + validation_info['time_remaining'] = 99999999 + validation_info['grace_period_remaining'] = 99999999 + + if validation_info.get('time_remaining', None) is None: + raise PermissionDenied("license is missing") + if validation_info.get("grace_period_remaining") <= 0: + raise PermissionDenied("license has expired") + if validation_info.get('free_instances', 0) < 0: + raise PermissionDenied("Host Count exceeds available instances") + + dep_access = self.user.can_access(Inventory, 'read', obj.inventory) and \ + self.user.can_access(Project, 'read', obj.project) + return self.can_read(obj) and dep_access + def can_change(self, obj, data): return self.can_read(obj) and self.can_add(data) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a6cc049696..5a21c1bc68 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -189,6 +189,18 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): needed.append(pw) return bool(self.credential and not len(needed)) + @property + def passwords_needed_to_start(self): + '''Return list of password field names needed to start the job.''' + needed = [] + if self.credential: + for pw in self.credential.passwords_needed: + if pw == 'password': + needed.append('ssh_password') + else: + needed.append(pw) + return needed + def _can_update(self): return self.can_start_without_user_input() diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 2722b53309..4f8d6ce869 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -302,7 +302,7 @@ class BaseTestMixin(object): self.assertFalse(response.content) #if return_response_object: # return response - if response.status_code not in [ 202, 204, 405 ] and method_name != 'head' and response.content: + if response.status_code not in [ 204, 405 ] and method_name != 'head' and response.content: # no JSON responses in these at least for now, 409 should probably return some (FIXME) if response['Content-Type'].startswith('application/json'): obj = json.loads(response.content) diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index b195294123..d9c74d41fa 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -681,6 +681,32 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): # FIXME: Check other credentials and optional fields. + def test_launch_job_template(self): + url = reverse('api:job_template_list') + data = dict( + name = 'launched job template', + job_type = PERM_INVENTORY_DEPLOY, + inventory = self.inv_eng.pk, + project = self.proj_dev.pk, + playbook = self.proj_dev.playbooks[0], + ) + with self.current_user(self.user_sue): + response = self.post(url, data, expect=201) + detail_url = reverse('api:job_template_detail', + args=(response['id'],)) + self.assertEquals(response['url'], detail_url) + + launch_url = reverse('api:job_template_launch', + args=(response['id'],)) + + # Invalid auth can't trigger the launch endpoint + self.check_invalid_auth(launch_url, {}, methods=('post',)) + + with self.current_user(self.user_sue): + response = self.post(launch_url, {}, expect=202) + j = Job.objects.get(pk=response['job']) + self.assertTrue(j.status == 'new') + class JobTest(BaseJobTestMixin, django.test.TestCase): def test_get_job_list(self):