mirror of
https://github.com/ansible/awx.git
synced 2024-10-27 00:55:06 +03:00
Initial working version of job template callback with a test.
This commit is contained in:
parent
552e43668c
commit
ca949eb71e
@ -1,7 +1,9 @@
|
|||||||
[run]
|
[run]
|
||||||
source = ansibleworks
|
source = awx
|
||||||
branch = True
|
branch = True
|
||||||
omit = ansibleworks/main/migrations/*
|
omit =
|
||||||
|
awx/main/migrations/*
|
||||||
|
awx/lib/site-packages/*
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
|
@ -412,6 +412,14 @@ class Credential(CommonModelNameNotUnique):
|
|||||||
@property
|
@property
|
||||||
def needs_sudo_password(self):
|
def needs_sudo_password(self):
|
||||||
return self.sudo_password == 'ASK'
|
return self.sudo_password == 'ASK'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def passwords_needed(self):
|
||||||
|
needed = []
|
||||||
|
for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'):
|
||||||
|
if getattr(self, 'needs_%s' % field):
|
||||||
|
needed.append(field)
|
||||||
|
return needed
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('main:credential_detail', args=(self.pk,))
|
return reverse('main:credential_detail', args=(self.pk,))
|
||||||
@ -646,14 +654,11 @@ class JobTemplate(CommonModel):
|
|||||||
return reverse('main:job_template_detail', args=(self.pk,))
|
return reverse('main:job_template_detail', args=(self.pk,))
|
||||||
|
|
||||||
def can_start_without_user_input(self):
|
def can_start_without_user_input(self):
|
||||||
'''Return whether job template can be used to start a new job without
|
'''
|
||||||
requiring any user input.'''
|
Return whether job template can be used to start a new job without
|
||||||
if not self.credential:
|
requiring any user input.
|
||||||
return False
|
'''
|
||||||
for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'):
|
return bool(self.credential and not self.credential.passwords_needed)
|
||||||
if getattr(self.credential, 'needs_%s' % field):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
class Job(CommonModel):
|
class Job(CommonModel):
|
||||||
'''
|
'''
|
||||||
@ -832,11 +837,7 @@ class Job(CommonModel):
|
|||||||
|
|
||||||
def get_passwords_needed_to_start(self):
|
def get_passwords_needed_to_start(self):
|
||||||
'''Return list of password field names needed to start the job.'''
|
'''Return list of password field names needed to start the job.'''
|
||||||
needed = []
|
return (self.credential and self.credential.passwords_needed) or []
|
||||||
for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'):
|
|
||||||
if self.credential and getattr(self.credential, 'needs_%s' % field):
|
|
||||||
needed.append(field)
|
|
||||||
return needed
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_start(self):
|
def can_start(self):
|
||||||
|
@ -127,36 +127,46 @@ class JobTemplateCallbackPermission(CustomRbac):
|
|||||||
def has_permission(self, request, view, obj=None):
|
def has_permission(self, request, view, obj=None):
|
||||||
# If another authentication method was used and it's not a POST, return
|
# If another authentication method was used and it's not a POST, return
|
||||||
# True to fall through to the next permission class.
|
# True to fall through to the next permission class.
|
||||||
if request.user or request.auth and request.method.lower() != 'post':
|
if (request.user or request.auth) and request.method.lower() != 'post':
|
||||||
return super(JobTemplateCallbackPermission, self).has_permission(request, view, obj)
|
return super(JobTemplateCallbackPermission, self).has_permission(request, view, obj)
|
||||||
|
|
||||||
return False
|
# Require method to be POST, host_config_key to be specified and match
|
||||||
# FIXME
|
# the requested job template, and require the job template to be
|
||||||
#try:
|
# active in order to proceed.
|
||||||
# job_template = JobTemplate.objects.get(active=True, pk=int(request.auth.split('-')[0]))
|
host_config_key = request.DATA.get('host_config_key', '')
|
||||||
#except Job.DoesNotExist:
|
if request.method.lower() != 'post':
|
||||||
# return False
|
return False
|
||||||
|
elif not host_config_key:
|
||||||
|
return False
|
||||||
|
elif obj and not obj.active:
|
||||||
|
return False
|
||||||
|
elif obj and obj.host_config_key != host_config_key:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
class JobTaskPermission(CustomRbac):
|
class JobTaskPermission(CustomRbac):
|
||||||
|
|
||||||
def has_permission(self, request, view, obj=None):
|
def has_permission(self, request, view, obj=None):
|
||||||
|
|
||||||
# If another authentication method was used other than the one for job
|
# If another authentication method was used other than the one for job
|
||||||
# callbacks, return True to fall through to the next permission class.
|
# callbacks, default to the superclass permissions checking.
|
||||||
if request.user or not request.auth:
|
if request.user or not request.auth:
|
||||||
return super(JobTaskPermission, self).has_permission(request, view, obj)
|
return super(JobTaskPermission, self).has_permission(request, view, obj)
|
||||||
|
|
||||||
# FIXME: Verify that inventory or job event requested are for the same
|
# Verify that the job ID present in the auth token is for a valid,
|
||||||
# job ID present in the auth token, etc.
|
# active job.
|
||||||
|
try:
|
||||||
#try:
|
job = Job.objects.get(active=True, status='running',
|
||||||
# job = Job.objects.get(active=True, status='running', pk=int(request.auth.split('-')[0]))
|
pk=int(request.auth.split('-')[0]))
|
||||||
#except Job.DoesNotExist:
|
except (Job.DoesNotExist, TypeError):
|
||||||
# return False
|
return False
|
||||||
|
|
||||||
|
# Verify that the request method is one of those allowed for the given
|
||||||
|
# view, also that the job or inventory being accessed matches the auth
|
||||||
|
# token.
|
||||||
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
if view.model == Inventory and request.method.lower() in ('head', 'get'):
|
||||||
return True
|
return bool(not obj or obj.pk == job.inventory.pk)
|
||||||
elif view.model == JobEvent and request.method.lower() == 'post':
|
elif view.model == JobEvent and request.method.lower() == 'post':
|
||||||
return True
|
return bool(not obj or obj.pk == job.pk)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -38,6 +38,7 @@ class RunJob(Task):
|
|||||||
if field == 'status':
|
if field == 'status':
|
||||||
update_fields.append('failed')
|
update_fields.append('failed')
|
||||||
job.save(update_fields=update_fields)
|
job.save(update_fields=update_fields)
|
||||||
|
# FIXME: Commit transaction?
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def get_path_to(self, *args):
|
def get_path_to(self, *args):
|
||||||
|
@ -145,7 +145,7 @@ class BaseTestMixin(object):
|
|||||||
return ('random', 'combination')
|
return ('random', 'combination')
|
||||||
|
|
||||||
def _generic_rest(self, url, data=None, expect=204, auth=None, method=None,
|
def _generic_rest(self, url, data=None, expect=204, auth=None, method=None,
|
||||||
data_type=None, accept=None):
|
data_type=None, accept=None, remote_addr=None):
|
||||||
assert method is not None
|
assert method is not None
|
||||||
method_name = method.lower()
|
method_name = method.lower()
|
||||||
if method_name not in ('options', 'head', 'get', 'delete'):
|
if method_name not in ('options', 'head', 'get', 'delete'):
|
||||||
@ -153,6 +153,8 @@ class BaseTestMixin(object):
|
|||||||
client_kwargs = {}
|
client_kwargs = {}
|
||||||
if accept:
|
if accept:
|
||||||
client_kwargs['HTTP_ACCEPT'] = accept
|
client_kwargs['HTTP_ACCEPT'] = accept
|
||||||
|
if remote_addr:
|
||||||
|
client_kwargs['REMOTE_ADDR'] = remote_addr
|
||||||
client = Client(**client_kwargs)
|
client = Client(**client_kwargs)
|
||||||
auth = auth or self._current_auth
|
auth = auth or self._current_auth
|
||||||
if auth:
|
if auth:
|
||||||
@ -191,39 +193,46 @@ class BaseTestMixin(object):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def options(self, url, expect=200, auth=None, accept=None):
|
def options(self, url, expect=200, auth=None, accept=None,
|
||||||
|
remote_addr=None):
|
||||||
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
||||||
method='options', accept=accept)
|
method='options', accept=accept,
|
||||||
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
def head(self, url, expect=200, auth=None, accept=None):
|
def head(self, url, expect=200, auth=None, accept=None, remote_addr=None):
|
||||||
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
||||||
method='head', accept=accept)
|
method='head', accept=accept,
|
||||||
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
def get(self, url, expect=200, auth=None, accept=None):
|
def get(self, url, expect=200, auth=None, accept=None, remote_addr=None):
|
||||||
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
||||||
method='get', accept=accept)
|
method='get', accept=accept,
|
||||||
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
def post(self, url, data, expect=204, auth=None, data_type=None,
|
def post(self, url, data, expect=204, auth=None, data_type=None,
|
||||||
accept=None):
|
accept=None, remote_addr=None):
|
||||||
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
||||||
method='post', data_type=data_type,
|
method='post', data_type=data_type,
|
||||||
accept=accept)
|
accept=accept,
|
||||||
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
def put(self, url, data, expect=200, auth=None, data_type=None,
|
def put(self, url, data, expect=200, auth=None, data_type=None,
|
||||||
accept=None):
|
accept=None, remote_addr=None):
|
||||||
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
||||||
method='put', data_type=data_type,
|
method='put', data_type=data_type,
|
||||||
accept=accept)
|
accept=accept, remote_addr=remote_addr)
|
||||||
|
|
||||||
def patch(self, url, data, expect=200, auth=None, data_type=None,
|
def patch(self, url, data, expect=200, auth=None, data_type=None,
|
||||||
accept=None):
|
accept=None, remote_addr=None):
|
||||||
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
return self._generic_rest(url, data=data, expect=expect, auth=auth,
|
||||||
method='patch', data_type=data_type,
|
method='patch', data_type=data_type,
|
||||||
accept=accept)
|
accept=accept, remote_addr=remote_addr)
|
||||||
|
|
||||||
def delete(self, url, expect=201, auth=None, data_type=None, accept=None):
|
def delete(self, url, expect=201, auth=None, data_type=None, accept=None,
|
||||||
|
remote_addr=None):
|
||||||
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
return self._generic_rest(url, data=None, expect=expect, auth=auth,
|
||||||
method='delete', accept=accept)
|
method='delete', accept=accept,
|
||||||
|
remote_addr=remote_addr)
|
||||||
|
|
||||||
def get_urls(self, collection_url, auth=None):
|
def get_urls(self, collection_url, auth=None):
|
||||||
# TODO: this test helper function doesn't support pagination
|
# TODO: this test helper function doesn't support pagination
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django
|
||||||
from django.contrib.auth.models import User as DjangoUser
|
from django.contrib.auth.models import User as DjangoUser
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
import django.test
|
import django.test
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
# AWX
|
||||||
from awx.main.models import *
|
from awx.main.models import *
|
||||||
from awx.main.tests.base import BaseTestMixin
|
from awx.main.tests.base import BaseTestMixin
|
||||||
|
|
||||||
__all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest']
|
__all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest',
|
||||||
|
'JobTemplateCallbackTest']
|
||||||
|
|
||||||
TEST_PLAYBOOK = '''- hosts: all
|
TEST_PLAYBOOK = '''- hosts: all
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
@ -308,6 +317,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
inventory= self.inv_eng,
|
inventory= self.inv_eng,
|
||||||
project=self.proj_dev,
|
project=self.proj_dev,
|
||||||
playbook=self.proj_dev.playbooks[0],
|
playbook=self.proj_dev.playbooks[0],
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_eng_check = self.jt_eng_check.create_job(
|
self.job_eng_check = self.jt_eng_check.create_job(
|
||||||
@ -320,6 +330,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
inventory= self.inv_eng,
|
inventory= self.inv_eng,
|
||||||
project=self.proj_dev,
|
project=self.proj_dev,
|
||||||
playbook=self.proj_dev.playbooks[0],
|
playbook=self.proj_dev.playbooks[0],
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_eng_run = self.jt_eng_run.create_job(
|
self.job_eng_run = self.jt_eng_run.create_job(
|
||||||
@ -335,6 +346,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
inventory= self.inv_sup,
|
inventory= self.inv_sup,
|
||||||
project=self.proj_test,
|
project=self.proj_test,
|
||||||
playbook=self.proj_test.playbooks[0],
|
playbook=self.proj_test.playbooks[0],
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_sup_check = self.jt_sup_check.create_job(
|
self.job_sup_check = self.jt_sup_check.create_job(
|
||||||
@ -347,6 +359,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
inventory= self.inv_sup,
|
inventory= self.inv_sup,
|
||||||
project=self.proj_test,
|
project=self.proj_test,
|
||||||
playbook=self.proj_test.playbooks[0],
|
playbook=self.proj_test.playbooks[0],
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_sup_run = self.jt_sup_run.create_job(
|
self.job_sup_run = self.jt_sup_run.create_job(
|
||||||
@ -363,6 +376,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
project=self.proj_prod,
|
project=self.proj_prod,
|
||||||
playbook=self.proj_prod.playbooks[0],
|
playbook=self.proj_prod.playbooks[0],
|
||||||
credential=self.cred_ops_east,
|
credential=self.cred_ops_east,
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_ops_east_check = self.jt_ops_east_check.create_job(
|
self.job_ops_east_check = self.jt_ops_east_check.create_job(
|
||||||
@ -375,6 +389,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
project=self.proj_prod,
|
project=self.proj_prod,
|
||||||
playbook=self.proj_prod.playbooks[0],
|
playbook=self.proj_prod.playbooks[0],
|
||||||
credential=self.cred_ops_east,
|
credential=self.cred_ops_east,
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_ops_east_run = self.jt_ops_east_run.create_job(
|
self.job_ops_east_run = self.jt_ops_east_run.create_job(
|
||||||
@ -387,6 +402,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
project=self.proj_prod,
|
project=self.proj_prod,
|
||||||
playbook=self.proj_prod.playbooks[0],
|
playbook=self.proj_prod.playbooks[0],
|
||||||
credential=self.cred_ops_west,
|
credential=self.cred_ops_west,
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_ops_west_check = self.jt_ops_west_check.create_job(
|
self.job_ops_west_check = self.jt_ops_west_check.create_job(
|
||||||
@ -399,6 +415,7 @@ class BaseJobTestMixin(BaseTestMixin):
|
|||||||
project=self.proj_prod,
|
project=self.proj_prod,
|
||||||
playbook=self.proj_prod.playbooks[0],
|
playbook=self.proj_prod.playbooks[0],
|
||||||
credential=self.cred_ops_west,
|
credential=self.cred_ops_west,
|
||||||
|
host_config_key=uuid.uuid4().hex,
|
||||||
created_by=self.user_sue,
|
created_by=self.user_sue,
|
||||||
)
|
)
|
||||||
self.job_ops_west_run = self.jt_ops_west_run.create_job(
|
self.job_ops_west_run = self.jt_ops_west_run.create_job(
|
||||||
@ -965,3 +982,142 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
|||||||
self.assertTrue(qs.count())
|
self.assertTrue(qs.count())
|
||||||
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)
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||||
|
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
|
||||||
|
ANSIBLE_TRANSPORT='local',
|
||||||
|
MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES)
|
||||||
|
class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||||
|
'''Job template callback tests for empheral hosts.'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(JobTemplateCallbackTest, self).setUp()
|
||||||
|
settings.INTERNAL_API_URL = self.live_server_url
|
||||||
|
# Monkeypatch socket module DNS lookup functions for testing.
|
||||||
|
self._original_gethostbyaddr = socket.gethostbyaddr
|
||||||
|
self._original_getaddrinfo = socket.getaddrinfo
|
||||||
|
socket.gethostbyaddr = self.gethostbyaddr
|
||||||
|
socket.getaddrinfo = self.getaddrinfo
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(JobTemplateCallbackTest, self).tearDown()
|
||||||
|
socket.gethostbyaddr = self._original_gethostbyaddr
|
||||||
|
socket.getaddrinfo = self._original_getaddrinfo
|
||||||
|
|
||||||
|
def atoh(self, a):
|
||||||
|
'''Convert IP address to integer in host byte order.'''
|
||||||
|
return socket.ntohl(struct.unpack('I', socket.inet_aton(a))[0])
|
||||||
|
|
||||||
|
def htoa(self, n):
|
||||||
|
'''Convert integer in host byte order to IP address.'''
|
||||||
|
return socket.inet_ntoa(struct.pack('I', socket.htonl(n)))
|
||||||
|
|
||||||
|
def get_test_ips_for_host(self, host):
|
||||||
|
'''Return test IP address(es) for given test hostname.'''
|
||||||
|
ips = []
|
||||||
|
try:
|
||||||
|
h = Host.objects.get(name=host)
|
||||||
|
# Primary IP for host (both forward/reverse lookups work).
|
||||||
|
val = self.atoh('127.10.0.0') + h.pk
|
||||||
|
ips.append(self.htoa(val))
|
||||||
|
# Secondary IP for host (both forward/reverse lookups work).
|
||||||
|
if h.pk % 2 == 0:
|
||||||
|
val = self.atoh('127.20.0.0') + h.pk
|
||||||
|
ips.append(self.htoa(val))
|
||||||
|
# Additional IP for host (only forward lookups work).
|
||||||
|
if h.pk % 3 == 0:
|
||||||
|
val = self.atoh('127.30.0.0') + h.pk
|
||||||
|
ips.append(self.htoa(val))
|
||||||
|
except Host.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return ips
|
||||||
|
|
||||||
|
def get_test_host_for_ip(self, ip):
|
||||||
|
'''Return test hostname for given test IP address.'''
|
||||||
|
if not ip.startswith('127.10.') and not ip.startswith('127.20.'):
|
||||||
|
return None
|
||||||
|
val = self.atoh(ip)
|
||||||
|
try:
|
||||||
|
return Host.objects.get(pk=(val & 0x0ffff)).name
|
||||||
|
except Host.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_dummy_host_ip_lookup(self):
|
||||||
|
all_ips = set()
|
||||||
|
for host in Host.objects.all():
|
||||||
|
ips = self.get_test_ips_for_host(host.name)
|
||||||
|
#print host, ips
|
||||||
|
self.assertTrue(ips)
|
||||||
|
all_ips.update(ips)
|
||||||
|
ips = self.get_test_ips_for_host('invalid_host_name')
|
||||||
|
self.assertFalse(ips)
|
||||||
|
for ip in all_ips:
|
||||||
|
host = self.get_test_host_for_ip(ip)
|
||||||
|
#print ip, host
|
||||||
|
if ip.startswith('127.30.'):
|
||||||
|
continue
|
||||||
|
self.assertTrue(host)
|
||||||
|
ips = self.get_test_ips_for_host(host)
|
||||||
|
self.assertTrue(ip in ips)
|
||||||
|
host = self.get_test_host_for_ip('127.10.254.254')
|
||||||
|
self.assertFalse(host)
|
||||||
|
|
||||||
|
def gethostbyaddr(self, ip):
|
||||||
|
#print 'gethostbyaddr', ip
|
||||||
|
if not ip.startswith('127.'):
|
||||||
|
return self._original_gethostbyaddr(ip)
|
||||||
|
host = self.get_test_host_for_ip(ip)
|
||||||
|
if not host:
|
||||||
|
raise socket.herror('unknown test host')
|
||||||
|
raddr = '.'.join(list(reversed(ip.split('.'))) + ['in-addr', 'arpa'])
|
||||||
|
return (host, [raddr], [ip])
|
||||||
|
|
||||||
|
def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0):
|
||||||
|
#print 'getaddrinfo', host, port, family, socktype, proto, flags
|
||||||
|
if family or socktype or proto or flags:
|
||||||
|
return self._original_getaddrinfo(host, port, family, socktype,
|
||||||
|
proto, flags)
|
||||||
|
port = port or 0
|
||||||
|
try:
|
||||||
|
socket.inet_aton(host)
|
||||||
|
addrs = [host]
|
||||||
|
except socket.error:
|
||||||
|
addrs = self.get_test_ips_for_host(host)
|
||||||
|
if not addrs:
|
||||||
|
raise socket.gaierror('test host not found')
|
||||||
|
results = []
|
||||||
|
for addr in addrs:
|
||||||
|
results.append((socket.AF_INET, socket.SOCK_STREAM,
|
||||||
|
socket.IPPROTO_TCP, '', (addr, port)))
|
||||||
|
results.append((socket.AF_INET, socket.SOCK_DGRAM,
|
||||||
|
socket.IPPROTO_UDP, '', (addr, port)))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def test_job_template_callback(self):
|
||||||
|
# Find a valid job template to use to test the callback.
|
||||||
|
job_template = None
|
||||||
|
qs = JobTemplate.objects.filter(job_type='run',
|
||||||
|
credential__isnull=False)
|
||||||
|
qs = qs.exclude(host_config_key='')
|
||||||
|
for jt in qs:
|
||||||
|
if not jt.can_start_without_user_input():
|
||||||
|
continue
|
||||||
|
job_template = jt
|
||||||
|
break
|
||||||
|
self.assertTrue(job_template)
|
||||||
|
url = reverse('main:job_template_callback', args=(job_template.pk,))
|
||||||
|
|
||||||
|
# Test a POST to start a new job.
|
||||||
|
with self.current_user(None):
|
||||||
|
data = dict(host_config_key=job_template.host_config_key)
|
||||||
|
host = job_template.inventory.hosts.order_by('-pk')[0]
|
||||||
|
ip = self.get_test_ips_for_host(host.name)[0]
|
||||||
|
jobs_qs = job_template.jobs.filter(launch_type='callback')
|
||||||
|
self.assertEqual(jobs_qs.count(), 0)
|
||||||
|
self.post(url, data, expect=202, remote_addr=ip)
|
||||||
|
self.assertEqual(jobs_qs.count(), 1)
|
||||||
|
job = jobs_qs[0]
|
||||||
|
self.assertEqual(job.launch_type, 'callback')
|
||||||
|
self.assertEqual(job.limit, host.name)
|
||||||
|
self.assertEqual(job.hosts.count(), 1)
|
||||||
|
self.assertEqual(job.hosts.all()[0], host)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
# Python
|
# Python
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
@ -1070,27 +1071,107 @@ class JobTemplateCallback(generics.RetrieveAPIView):
|
|||||||
'''
|
'''
|
||||||
Configure a host to POST to this resource using the `host_config_key`.
|
Configure a host to POST to this resource using the `host_config_key`.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
permission_classes = (JobTemplateCallbackPermission,)
|
permission_classes = (JobTemplateCallbackPermission,)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def find_host(self):
|
||||||
|
'''
|
||||||
|
Find the host in the job template's inventory that matches the remote
|
||||||
|
host for the current request.
|
||||||
|
'''
|
||||||
|
# Find the list of remote host names/IPs to check.
|
||||||
|
remote_hosts = set()
|
||||||
|
for header in settings.REMOTE_HOST_HEADERS:
|
||||||
|
value = self.request.META.get(header, '').strip()
|
||||||
|
if value:
|
||||||
|
remote_hosts.add(value)
|
||||||
|
# Add the reverse lookup of IP addresses.
|
||||||
|
for rh in list(remote_hosts):
|
||||||
|
try:
|
||||||
|
result = socket.gethostbyaddr(rh)
|
||||||
|
except socket.herror:
|
||||||
|
continue
|
||||||
|
remote_hosts.add(result[0])
|
||||||
|
remote_hosts.update(result[1])
|
||||||
|
# Filter out any .arpa results.
|
||||||
|
for rh in list(remote_hosts):
|
||||||
|
if rh.endswith('.arpa'):
|
||||||
|
remote_hosts.remove(rh)
|
||||||
|
if not remote_hosts:
|
||||||
|
return
|
||||||
|
# Find the host objects to search for a match.
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
qs = obj.inventory.hosts.filter(active=True)
|
||||||
|
# First try for an exact match on the name.
|
||||||
|
try:
|
||||||
|
return qs.get(name__in=remote_hosts)
|
||||||
|
except (Host.DoesNotExist, Host.MultipleObjectsReturned):
|
||||||
|
pass
|
||||||
|
# Next, try matching based on name or ansible_ssh_host variable.
|
||||||
|
matches = dict()
|
||||||
|
for host in qs:
|
||||||
|
ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '')
|
||||||
|
if ansible_ssh_host in remote_hosts:
|
||||||
|
if host not in matches:
|
||||||
|
matches[host] = 0
|
||||||
|
matches[host] += 2
|
||||||
|
if host.name != ansible_ssh_host and host.name in remote_hosts:
|
||||||
|
if host not in matches:
|
||||||
|
matches[host] = 0
|
||||||
|
matches[host] += 1
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches.keys()[0]
|
||||||
|
# Try to resolve forward addresses for each host to find a match.
|
||||||
|
for host in qs:
|
||||||
|
hostnames = set([host.name])
|
||||||
|
ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '')
|
||||||
|
if ansible_ssh_host:
|
||||||
|
hostnames.add(ansible_ssh_host)
|
||||||
|
for hostname in hostnames:
|
||||||
|
try:
|
||||||
|
result = socket.getaddrinfo(hostname, None)
|
||||||
|
possible_ips = set(x[4][0] for x in result)
|
||||||
|
possible_ips.discard(hostname)
|
||||||
|
if possible_ips and possible_ips & remote_hosts:
|
||||||
|
if host in matches:
|
||||||
|
matches[host] += 1
|
||||||
|
else:
|
||||||
|
matches[host] = 1
|
||||||
|
except socket.gaierror:
|
||||||
|
pass
|
||||||
|
# Return the host with the highest match weight (in case of multiple
|
||||||
|
# matches).
|
||||||
|
if matches:
|
||||||
|
return sorted(matches.items(), key=lambda x: x[1])[-1][0]
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
job_template = self.get_object()
|
||||||
data = dict(
|
data = dict(
|
||||||
host_config_key=obj.host_config_key,
|
host_config_key=job_template.host_config_key,
|
||||||
|
matched_host=getattr(self.find_host(), 'name', None),
|
||||||
)
|
)
|
||||||
|
if settings.DEBUG:
|
||||||
|
d = dict([(k,v) for k,v in request.META.items()
|
||||||
|
if k.startswith('HTTP_') or k.startswith('REMOTE_')])
|
||||||
|
data['request_meta'] = d
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
job_template = self.get_object()
|
||||||
# Permission class should have already validated host_config_key.
|
# Permission class should have already validated host_config_key.
|
||||||
# FIXME: Find host from request.
|
host = self.find_host()
|
||||||
limit = obj.limit
|
if not host:
|
||||||
# FIXME: Update limit based on host.
|
data = dict(msg='No matching host could be found!')
|
||||||
job = obj.create_job(limit=limit, launch_type='callback')
|
return Response(data, status=400)
|
||||||
|
if not job_template.can_start_without_user_input():
|
||||||
|
data = dict(msg='Cannot start automatically, user input required!')
|
||||||
|
return Response(data, status=400)
|
||||||
|
limit = ':'.join(filter(None, [job_template.limit, host.name]))
|
||||||
|
job = job_template.create_job(limit=limit, launch_type='callback')
|
||||||
result = job.start()
|
result = job.start()
|
||||||
if not result:
|
if not result:
|
||||||
data = dict(passwords_needed_to_start=job.get_passwords_needed_to_start())
|
data = dict(msg='Error starting job!')
|
||||||
return Response(data, status=400)
|
return Response(data, status=400)
|
||||||
else:
|
else:
|
||||||
return Response(status=202)
|
return Response(status=202)
|
||||||
|
@ -92,6 +92,11 @@ SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
|
|||||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
# HTTP headers and meta keys to search to determine remote host name or IP. Add
|
||||||
|
# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a
|
||||||
|
# reverse proxy.
|
||||||
|
REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS += (
|
TEMPLATE_CONTEXT_PROCESSORS += (
|
||||||
'django.core.context_processors.request',
|
'django.core.context_processors.request',
|
||||||
'awx.ui.context_processors.settings',
|
'awx.ui.context_processors.settings',
|
||||||
|
@ -55,6 +55,11 @@ LANGUAGE_CODE = 'en-us'
|
|||||||
# the secret key from an environment variable or a file instead.
|
# the secret key from an environment variable or a file instead.
|
||||||
SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
|
SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
|
||||||
|
|
||||||
|
# HTTP headers and meta keys to search to determine remote host name or IP. Add
|
||||||
|
# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a
|
||||||
|
# reverse proxy.
|
||||||
|
REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST']
|
||||||
|
|
||||||
# Email address that error messages come from.
|
# Email address that error messages come from.
|
||||||
SERVER_EMAIL = 'root@localhost'
|
SERVER_EMAIL = 'root@localhost'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user