mirror of
https://github.com/ansible/awx.git
synced 2026-05-17 14:27:42 -02:30
Work in progress on inventory script using API.
This commit is contained in:
29
ansibleworks/main/authentication.py
Normal file
29
ansibleworks/main/authentication.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Django REST Framework
|
||||||
|
from rest_framework import authentication
|
||||||
|
from rest_framework import exceptions
|
||||||
|
|
||||||
|
# AnsibleWorks
|
||||||
|
from ansibleworks.main.models import Job
|
||||||
|
|
||||||
|
class JobCallbackAuthentication(authentication.BaseAuthentication):
|
||||||
|
'''
|
||||||
|
Custom authentication used for views accessed by the inventory and callback
|
||||||
|
scripts when running a job.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
auth = authentication.get_authorization_header(request).split()
|
||||||
|
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
|
||||||
|
return None
|
||||||
|
job_id, job_key = auth[1].split('-', 1)
|
||||||
|
try:
|
||||||
|
job = Job.objects.get(pk=job_id, status='running')
|
||||||
|
except Job.DoesNotExist:
|
||||||
|
return None
|
||||||
|
token = job.callback_auth_token
|
||||||
|
if auth[1] != token:
|
||||||
|
raise exceptions.AuthenticationFailed('Invalid job callback token')
|
||||||
|
return (None, token)
|
||||||
|
|
||||||
|
def authenticate_header(self, request):
|
||||||
|
return 'Token'
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
@@ -814,6 +815,13 @@ class Job(CommonModel):
|
|||||||
except TaskMeta.DoesNotExist:
|
except TaskMeta.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def callback_auth_token(self):
|
||||||
|
'''Return temporary auth token used for task callbacks via API.'''
|
||||||
|
if self.status == 'running':
|
||||||
|
h = hmac.new(settings.SECRET_KEY, self.created.isoformat())
|
||||||
|
return '%d-%s' % (self.pk, h.hexdigest())
|
||||||
|
|
||||||
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 = []
|
needed = []
|
||||||
|
|||||||
@@ -116,3 +116,15 @@ class CustomRbac(permissions.BasePermission):
|
|||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
return self.has_permission(request, view, obj)
|
return self.has_permission(request, view, obj)
|
||||||
|
|
||||||
|
class JobCallbackPermission(CustomRbac):
|
||||||
|
|
||||||
|
def has_permission(self, request, view, obj=None):
|
||||||
|
# If another authentication method was used other than the one for job
|
||||||
|
# callbacks, return True to fall through to the next permission class.
|
||||||
|
if request.user or not request.auth:
|
||||||
|
return super(JobCallbackPermission, self).has_permission(request, view, obj)
|
||||||
|
# FIXME: Verify that inventory or job event requested are for the same
|
||||||
|
# job ID present in the auth token, etc.
|
||||||
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -86,10 +86,13 @@ class RunJob(Task):
|
|||||||
# answer: TBD
|
# answer: TBD
|
||||||
env['ACOM_JOB_ID'] = str(job.pk)
|
env['ACOM_JOB_ID'] = str(job.pk)
|
||||||
env['ACOM_INVENTORY_ID'] = str(job.inventory.pk)
|
env['ACOM_INVENTORY_ID'] = str(job.inventory.pk)
|
||||||
|
env['INVENTORY_ID'] = str(job.inventory.pk)
|
||||||
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
|
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
|
||||||
env['ACOM_CALLBACK_EVENT_SCRIPT'] = callback_script
|
env['ACOM_CALLBACK_EVENT_SCRIPT'] = callback_script
|
||||||
if hasattr(settings, 'ANSIBLE_TRANSPORT'):
|
if hasattr(settings, 'ANSIBLE_TRANSPORT'):
|
||||||
env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT')
|
env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT')
|
||||||
|
env['REST_API_URL'] = settings.INTERNAL_API_URL
|
||||||
|
env['REST_API_TOKEN'] = job.callback_auth_token or ''
|
||||||
env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences.
|
env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences.
|
||||||
return env
|
return env
|
||||||
|
|
||||||
@@ -108,8 +111,11 @@ class RunJob(Task):
|
|||||||
# it doesn't make sense to rely on ansible-playbook's default of using
|
# it doesn't make sense to rely on ansible-playbook's default of using
|
||||||
# the current user.
|
# the current user.
|
||||||
ssh_username = ssh_username or 'root'
|
ssh_username = ssh_username or 'root'
|
||||||
inventory_script = self.get_path_to('management', 'commands',
|
if False:
|
||||||
'acom_inventory.py')
|
inventory_script = self.get_path_to('management', 'commands',
|
||||||
|
'acom_inventory.py')
|
||||||
|
else:
|
||||||
|
inventory_script = self.get_path_to('..', 'scripts', 'inventory.py')
|
||||||
args = ['ansible-playbook', '-i', inventory_script]
|
args = ['ansible-playbook', '-i', inventory_script]
|
||||||
if job.job_type == 'check':
|
if job.job_type == 'check':
|
||||||
args.append('--check')
|
args.append('--check')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ansibleworks.main.tests.users import UsersTest
|
|||||||
from ansibleworks.main.tests.inventory import InventoryTest
|
from ansibleworks.main.tests.inventory import InventoryTest
|
||||||
from ansibleworks.main.tests.projects import ProjectsTest
|
from ansibleworks.main.tests.projects import ProjectsTest
|
||||||
from ansibleworks.main.tests.commands import *
|
from ansibleworks.main.tests.commands import *
|
||||||
|
from ansibleworks.main.tests.scripts import *
|
||||||
from ansibleworks.main.tests.tasks import RunJobTest
|
from ansibleworks.main.tests.tasks import RunJobTest
|
||||||
from ansibleworks.main.tests.jobs import *
|
from ansibleworks.main.tests.jobs import *
|
||||||
|
|
||||||
|
|||||||
@@ -240,3 +240,8 @@ class BaseTransactionTest(BaseTestMixin, django.test.TransactionTestCase):
|
|||||||
Base class for tests requiring transactions (or where the test database
|
Base class for tests requiring transactions (or where the test database
|
||||||
needs to be accessed by subprocesses).
|
needs to be accessed by subprocesses).
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase):
|
||||||
|
'''
|
||||||
|
Base class for tests requiring a live test server.
|
||||||
|
'''
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from django.utils.timezone import now
|
|||||||
from ansibleworks.main.models import *
|
from ansibleworks.main.models import *
|
||||||
from ansibleworks.main.tests.base import BaseTest
|
from ansibleworks.main.tests.base import BaseTest
|
||||||
|
|
||||||
__all__ = ['RunCommandAsScriptTest', 'AcomInventoryTest',
|
__all__ = ['RunCommandAsScriptTest',# 'AcomInventoryTest',
|
||||||
'AcomCallbackEventTest']
|
'AcomCallbackEventTest']
|
||||||
|
|
||||||
class BaseCommandTest(BaseTest):
|
class BaseCommandTest(BaseTest):
|
||||||
|
|||||||
271
ansibleworks/main/tests/scripts.py
Normal file
271
ansibleworks/main/tests/scripts.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import StringIO
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
# AnsibleWorks
|
||||||
|
from ansibleworks.main.models import *
|
||||||
|
from ansibleworks.main.tests.base import BaseLiveServerTest
|
||||||
|
|
||||||
|
__all__ = ['InventoryScriptTest']
|
||||||
|
|
||||||
|
class BaseScriptTest(BaseLiveServerTest):
|
||||||
|
'''
|
||||||
|
Base class for tests that run external scripts to access the API.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BaseScriptTest, self).setUp()
|
||||||
|
self._sys_path = [x for x in sys.path]
|
||||||
|
self._environ = dict(os.environ.items())
|
||||||
|
self._temp_files = []
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(BaseScriptTest, self).tearDown()
|
||||||
|
sys.path = self._sys_path
|
||||||
|
for k,v in self._environ.items():
|
||||||
|
if os.environ.get(k, None) != v:
|
||||||
|
os.environ[k] = v
|
||||||
|
for k,v in os.environ.items():
|
||||||
|
if k not in self._environ.keys():
|
||||||
|
del os.environ[k]
|
||||||
|
for tf in self._temp_files:
|
||||||
|
if os.path.exists(tf):
|
||||||
|
os.remove(tf)
|
||||||
|
|
||||||
|
def run_script(self, name, *args, **options):
|
||||||
|
'''
|
||||||
|
Run an external script and capture its stdout/stderr and return code.
|
||||||
|
'''
|
||||||
|
#stdin_fileobj = options.pop('stdin_fileobj', None)
|
||||||
|
pargs = [name]
|
||||||
|
for k,v in options.items():
|
||||||
|
pargs.append('%s%s' % ('-' if len(k) == 1 else '--', k))
|
||||||
|
if not v is True:
|
||||||
|
pargs.append(str(v))
|
||||||
|
for arg in args:
|
||||||
|
pargs.append(str(arg))
|
||||||
|
proc = subprocess.Popen(pargs, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = proc.communicate()
|
||||||
|
return proc.returncode, stdout, stderr
|
||||||
|
|
||||||
|
class InventoryScriptTest(BaseScriptTest):
|
||||||
|
'''
|
||||||
|
Test helper to run management command as standalone script.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(InventoryScriptTest, self).setUp()
|
||||||
|
self.setup_users()
|
||||||
|
self.organizations = self.make_organizations(self.super_django_user, 2)
|
||||||
|
self.projects = self.make_projects(self.normal_django_user, 2)
|
||||||
|
self.organizations[0].projects.add(self.projects[1])
|
||||||
|
self.organizations[1].projects.add(self.projects[0])
|
||||||
|
self.inventories = []
|
||||||
|
self.hosts = []
|
||||||
|
self.groups = []
|
||||||
|
for n, organization in enumerate(self.organizations):
|
||||||
|
inventory = Inventory.objects.create(name='inventory-%d' % n,
|
||||||
|
description='description for inventory %d' % n,
|
||||||
|
organization=organization,
|
||||||
|
variables=json.dumps({'n': n}) if n else '')
|
||||||
|
self.inventories.append(inventory)
|
||||||
|
hosts = []
|
||||||
|
for x in xrange(10):
|
||||||
|
if n > 0:
|
||||||
|
variables = json.dumps({'ho': 'hum-%d' % x})
|
||||||
|
else:
|
||||||
|
variables = ''
|
||||||
|
host = inventory.hosts.create(name='host-%02d-%02d.example.com' % (n, x),
|
||||||
|
inventory=inventory,
|
||||||
|
variables=variables)
|
||||||
|
if x in (3, 7):
|
||||||
|
host.mark_inactive()
|
||||||
|
hosts.append(host)
|
||||||
|
self.hosts.extend(hosts)
|
||||||
|
groups = []
|
||||||
|
for x in xrange(5):
|
||||||
|
if n > 0:
|
||||||
|
variables = json.dumps({'gee': 'whiz-%d' % x})
|
||||||
|
else:
|
||||||
|
variables = ''
|
||||||
|
group = inventory.groups.create(name='group-%d' % x,
|
||||||
|
inventory=inventory,
|
||||||
|
variables=variables)
|
||||||
|
if x == 2:
|
||||||
|
group.mark_inactive()
|
||||||
|
groups.append(group)
|
||||||
|
group.hosts.add(hosts[x])
|
||||||
|
group.hosts.add(hosts[x + 5])
|
||||||
|
if n > 0 and x == 4:
|
||||||
|
group.parents.add(groups[3])
|
||||||
|
self.groups.extend(groups)
|
||||||
|
|
||||||
|
def run_inventory_script(self, *args, **options):
|
||||||
|
os.environ.setdefault('REST_API_URL', self.live_server_url)
|
||||||
|
os.environ.setdefault('REST_API_TOKEN',
|
||||||
|
self.super_django_user.auth_token.key)
|
||||||
|
name = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts',
|
||||||
|
'inventory.py')
|
||||||
|
return self.run_script(name, *args, **options)
|
||||||
|
|
||||||
|
def test_without_inventory_id(self):
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=self.hosts[0].name)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
|
def test_list_with_inventory_id_as_argument(self):
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True,
|
||||||
|
inventory=inventory.pk)
|
||||||
|
self.assertEqual(rc, 0, stderr)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
groups = inventory.groups.filter(active=True)
|
||||||
|
groupnames = groups.values_list('name', flat=True)
|
||||||
|
self.assertEqual(set(data.keys()), set(groupnames))
|
||||||
|
# Groups for this inventory should only have hosts, and no group
|
||||||
|
# variable data or parent/child relationships.
|
||||||
|
for k,v in data.items():
|
||||||
|
self.assertTrue(isinstance(v, (list, tuple)))
|
||||||
|
group = inventory.groups.get(active=True, name=k)
|
||||||
|
hosts = group.hosts.filter(active=True)
|
||||||
|
hostnames = hosts.values_list('name', flat=True)
|
||||||
|
self.assertEqual(set(v), set(hostnames))
|
||||||
|
for group in inventory.groups.filter(active=False):
|
||||||
|
self.assertFalse(group.name in data.keys(),
|
||||||
|
'deleted group %s should not be in data' % group)
|
||||||
|
# Command line argument for inventory ID should take precedence over
|
||||||
|
# environment variable.
|
||||||
|
inventory_pks = set(map(lambda x: x.pk, self.inventories))
|
||||||
|
invalid_id = [x for x in xrange(9999) if x not in inventory_pks][0]
|
||||||
|
os.environ['INVENTORY_ID'] = str(invalid_id)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True,
|
||||||
|
inventory=inventory.pk)
|
||||||
|
self.assertEqual(rc, 0, stderr)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
|
||||||
|
def test_list_with_inventory_id_in_environment(self):
|
||||||
|
inventory = self.inventories[1]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||||
|
self.assertEqual(rc, 0, stderr)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
groups = inventory.groups.filter(active=True)
|
||||||
|
groupnames = list(groups.values_list('name', flat=True)) + ['all']
|
||||||
|
self.assertEqual(set(data.keys()), set(groupnames))
|
||||||
|
# Groups for this inventory should have hosts, variable data, and one
|
||||||
|
# parent/child relationship.
|
||||||
|
for k,v in data.items():
|
||||||
|
self.assertTrue(isinstance(v, dict))
|
||||||
|
if k == 'all':
|
||||||
|
self.assertEqual(v.get('vars', {}), inventory.variables_dict)
|
||||||
|
continue
|
||||||
|
group = inventory.groups.get(active=True, name=k)
|
||||||
|
hosts = group.hosts.filter(active=True)
|
||||||
|
hostnames = hosts.values_list('name', flat=True)
|
||||||
|
self.assertEqual(set(v.get('hosts', [])), set(hostnames))
|
||||||
|
if group.variables:
|
||||||
|
self.assertEqual(v.get('vars', {}), group.variables_dict)
|
||||||
|
if k == 'group-3':
|
||||||
|
children = group.children.filter(active=True)
|
||||||
|
childnames = children.values_list('name', flat=True)
|
||||||
|
self.assertEqual(set(v.get('children', [])), set(childnames))
|
||||||
|
else:
|
||||||
|
self.assertFalse('children' in v)
|
||||||
|
|
||||||
|
def test_valid_host(self):
|
||||||
|
# Host without variable data.
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
host = inventory.hosts.filter(active=True)[2]
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||||
|
self.assertEqual(rc, 0, stderr)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
self.assertEqual(data, {})
|
||||||
|
# Host with variable data.
|
||||||
|
inventory = self.inventories[1]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
host = inventory.hosts.filter(active=True)[4]
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||||
|
self.assertEqual(rc, 0, stderr)
|
||||||
|
data = json.loads(stdout)
|
||||||
|
self.assertEqual(data, host.variables_dict)
|
||||||
|
|
||||||
|
def test_invalid_host(self):
|
||||||
|
# Valid host, but not part of the specified inventory.
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
host = Host.objects.exclude(inventory=inventory)[0]
|
||||||
|
self.assertTrue(host.active)
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=host.name)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
# Invalid hostname not in database.
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host='blah.example.com')
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
|
def test_with_invalid_inventory_id(self):
|
||||||
|
inventory_pks = set(map(lambda x: x.pk, self.inventories))
|
||||||
|
invalid_id = [x for x in xrange(1, 9999) if x not in inventory_pks][0]
|
||||||
|
os.environ['INVENTORY_ID'] = str(invalid_id)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
os.environ['INVENTORY_ID'] = 'not_an_int'
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
os.environ['INVENTORY_ID'] = str(invalid_id)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=self.hosts[1].name)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
os.environ['INVENTORY_ID'] = 'not_an_int'
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(host=self.hosts[2].name)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
|
def test_with_deleted_inventory(self):
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
inventory.mark_inactive()
|
||||||
|
self.assertFalse(inventory.active)
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True)
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
|
def test_without_list_or_host_argument(self):
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script()
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
|
def _test_with_both_list_and_host_arguments(self):
|
||||||
|
inventory = self.inventories[0]
|
||||||
|
self.assertTrue(inventory.active)
|
||||||
|
os.environ['INVENTORY_ID'] = str(inventory.pk)
|
||||||
|
rc, stdout, stderr = self.run_inventory_script(list=True, host='blah')
|
||||||
|
self.assertNotEqual(rc, 0, stderr)
|
||||||
|
self.assertEqual(json.loads(stdout), {})
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import tempfile
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from ansibleworks.main.models import *
|
from ansibleworks.main.models import *
|
||||||
from ansibleworks.main.tests.base import BaseTransactionTest
|
from ansibleworks.main.tests.base import BaseTransactionTest, BaseLiveServerTest
|
||||||
from ansibleworks.main.tasks import RunJob
|
from ansibleworks.main.tasks import RunJob
|
||||||
|
|
||||||
TEST_PLAYBOOK = '''- hosts: test-group
|
TEST_PLAYBOOK = '''- hosts: test-group
|
||||||
@@ -90,7 +90,7 @@ TEST_SSH_KEY_DATA_UNLOCK = 'unlockme'
|
|||||||
|
|
||||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||||
class BaseCeleryTest(BaseTransactionTest):
|
class BaseCeleryTest(BaseLiveServerTest):#BaseTransactionTest):
|
||||||
'''
|
'''
|
||||||
Base class for celery task tests.
|
Base class for celery task tests.
|
||||||
'''
|
'''
|
||||||
@@ -128,6 +128,7 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
self.build_args_callback()
|
self.build_args_callback()
|
||||||
return args
|
return args
|
||||||
RunJob.build_args = new_build_args
|
RunJob.build_args = new_build_args
|
||||||
|
settings.INTERNAL_API_URL = self.live_server_url
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(RunJobTest, self).tearDown()
|
super(RunJobTest, self).tearDown()
|
||||||
@@ -193,6 +194,7 @@ class RunJobTest(BaseCeleryTest):
|
|||||||
expect_traceback=False):
|
expect_traceback=False):
|
||||||
msg = 'job status is %s, expected %s' % (job.status, expected)
|
msg = 'job status is %s, expected %s' % (job.status, expected)
|
||||||
msg = '%s\nargs:\n%s' % (msg, job.job_args)
|
msg = '%s\nargs:\n%s' % (msg, job.job_args)
|
||||||
|
msg = '%s\nenv:\n%s' % (msg, job.job_env)
|
||||||
if job.result_traceback:
|
if job.result_traceback:
|
||||||
msg = '%s\ngot traceback:\n%s' % (msg, job.result_traceback)
|
msg = '%s\ngot traceback:\n%s' % (msg, job.result_traceback)
|
||||||
if job.result_stdout:
|
if job.result_stdout:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ inventory_urls = patterns('ansibleworks.main.views',
|
|||||||
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/root_groups/$', 'inventory_root_groups_list'),
|
url(r'^(?P<pk>[0-9]+)/root_groups/$', 'inventory_root_groups_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_detail'),
|
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_detail'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/script/$', 'inventory_script_view'),
|
||||||
)
|
)
|
||||||
|
|
||||||
host_urls = patterns('ansibleworks.main.views',
|
host_urls = patterns('ansibleworks.main.views',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
# AnsibleWorks
|
# AnsibleWorks
|
||||||
from ansibleworks.main.access import *
|
from ansibleworks.main.access import *
|
||||||
|
from ansibleworks.main.authentication import JobCallbackAuthentication
|
||||||
from ansibleworks.main.base_views import *
|
from ansibleworks.main.base_views import *
|
||||||
from ansibleworks.main.models import *
|
from ansibleworks.main.models import *
|
||||||
from ansibleworks.main.rbac import *
|
from ansibleworks.main.rbac import *
|
||||||
@@ -983,6 +984,53 @@ class GroupVariableDetail(BaseVariableDetail):
|
|||||||
model = Group
|
model = Group
|
||||||
serializer_class = GroupVariableDataSerializer
|
serializer_class = GroupVariableDataSerializer
|
||||||
|
|
||||||
|
class InventoryScriptView(generics.RetrieveAPIView):
|
||||||
|
'''
|
||||||
|
Return inventory group and host data as needed for an inventory script.
|
||||||
|
|
||||||
|
Without query parameters, return groups with hosts, children and vars
|
||||||
|
(equivalent to the --list parameter to an inventory script).
|
||||||
|
|
||||||
|
With ?host=HOSTNAME, return host vars for the given host (equivalent to the
|
||||||
|
--host HOSTNAME parameter to an inventory script).
|
||||||
|
'''
|
||||||
|
|
||||||
|
model = Inventory
|
||||||
|
authentication_classes = [JobCallbackAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||||
|
permission_classes = (JobCallbackPermission,)
|
||||||
|
filter_backends = ()
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
hostname = request.QUERY_PARAMS.get('host', '')
|
||||||
|
if hostname:
|
||||||
|
try:
|
||||||
|
host = self.object.hosts.get(active=True, name=hostname)
|
||||||
|
data = host.variables_dict
|
||||||
|
except Host.DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
for group in self.object.groups.filter(active=True):
|
||||||
|
hosts = group.hosts.filter(active=True)
|
||||||
|
children = group.children.filter(active=True)
|
||||||
|
group_info = {
|
||||||
|
'hosts': list(hosts.values_list('name', flat=True)),
|
||||||
|
'children': list(children.values_list('name', flat=True)),
|
||||||
|
'vars': group.variables_dict,
|
||||||
|
}
|
||||||
|
group_info = dict(filter(lambda x: bool(x[1]),
|
||||||
|
group_info.items()))
|
||||||
|
if group_info.keys() in ([], ['hosts']):
|
||||||
|
data[group.name] = group_info.get('hosts', [])
|
||||||
|
else:
|
||||||
|
data[group.name] = group_info
|
||||||
|
if self.object.variables_dict:
|
||||||
|
data['all'] = {
|
||||||
|
'vars': self.object.variables_dict,
|
||||||
|
}
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
class JobTemplateList(BaseList):
|
class JobTemplateList(BaseList):
|
||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
|
|||||||
120
ansibleworks/scripts/inventory.py
Executable file
120
ansibleworks/scripts/inventory.py
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Python
|
||||||
|
import json
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
# Requests
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class TokenAuth(requests.auth.AuthBase):
|
||||||
|
def __init__(self, token):
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
request.headers['Authorization'] = 'Token %s' % self.token
|
||||||
|
return request
|
||||||
|
|
||||||
|
class InventoryScript(object):
|
||||||
|
|
||||||
|
def __init__(self, **options):
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
parts = urlparse.urlsplit(self.base_url)
|
||||||
|
if parts.username and parts.password:
|
||||||
|
auth = (parts.username, parts.password)
|
||||||
|
elif self.auth_token:
|
||||||
|
auth = TokenAuth(self.auth_token)
|
||||||
|
else:
|
||||||
|
auth = None
|
||||||
|
url = urlparse.urlunsplit([parts.scheme,
|
||||||
|
'%s:%d' % (parts.hostname, parts.port),
|
||||||
|
parts.path, parts.query, parts.fragment])
|
||||||
|
url_path = '/api/v1/inventories/%d/script/' % self.inventory_id
|
||||||
|
if self.hostname:
|
||||||
|
url_path += '?%s' % urllib.urlencode({'host': self.hostname})
|
||||||
|
url = urlparse.urljoin(url, url_path)
|
||||||
|
response = requests.get(url, auth=auth)
|
||||||
|
response.raise_for_status()
|
||||||
|
sys.stdout.write(json.dumps(response.json(), indent=self.indent) + '\n')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self.base_url = self.options.get('base_url', '') or \
|
||||||
|
os.getenv('REST_API_URL', '')
|
||||||
|
if not self.base_url:
|
||||||
|
raise ValueError('No REST API URL specified')
|
||||||
|
self.auth_token = self.options.get('authtoken', '') or \
|
||||||
|
os.getenv('REST_API_TOKEN', '')
|
||||||
|
parts = urlparse.urlsplit(self.base_url)
|
||||||
|
if not (parts.username and parts.password) and not self.auth_token:
|
||||||
|
raise ValueError('No REST API token or username/password '
|
||||||
|
'specified')
|
||||||
|
try:
|
||||||
|
# Command line argument takes precedence over environment
|
||||||
|
# variable.
|
||||||
|
self.inventory_id = int(self.options.get('inventory_id', 0) or \
|
||||||
|
os.getenv('INVENTORY_ID', 0))
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Inventory ID must be an integer')
|
||||||
|
if not self.inventory_id:
|
||||||
|
raise ValueError('No inventory ID specified')
|
||||||
|
self.hostname = self.options.get('hostname', '')
|
||||||
|
self.list_ = self.options.get('list', False)
|
||||||
|
self.indent = self.options.get('indent', None)
|
||||||
|
if self.list_ and self.hostname:
|
||||||
|
raise RuntimeError('Only --list or --host may be specified')
|
||||||
|
elif self.list_ or self.hostname:
|
||||||
|
self.get_data()
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Either --list or --host must be specified')
|
||||||
|
except Exception, e:
|
||||||
|
# Always return an empty hash on stdout, even when an error occurs.
|
||||||
|
sys.stdout.write(json.dumps({}))
|
||||||
|
#print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), repr(e)
|
||||||
|
#if hasattr(e, 'response'):
|
||||||
|
# print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), e.response.content
|
||||||
|
if self.options.get('traceback', False):
|
||||||
|
raise
|
||||||
|
sys.stderr.write(str(e) + '\n')
|
||||||
|
if hasattr(e, 'response'):
|
||||||
|
sys.stderr.write(e.response.content + '\n')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = optparse.OptionParser()
|
||||||
|
parser.add_option('-v', '--verbosity', action='store', dest='verbosity',
|
||||||
|
default='1', type='choice', choices=['0', '1', '2', '3'],
|
||||||
|
help='Verbosity level; 0=minimal output, 1=normal output'
|
||||||
|
', 2=verbose output, 3=very verbose output')
|
||||||
|
parser.add_option('--traceback', action='store_true',
|
||||||
|
help='Raise on exception on error')
|
||||||
|
parser.add_option('-u', '--url', dest='base_url', default='',
|
||||||
|
help='Base URL to access REST API (can also be specified'
|
||||||
|
' using REST_API_URL environment variable)')
|
||||||
|
parser.add_option('--authtoken', dest='authtoken', default='',
|
||||||
|
help='Authentication token used to access REST API (can '
|
||||||
|
'also be specified using REST_API_TOKEN environment '
|
||||||
|
'variable)')
|
||||||
|
parser.add_option('-i', '--inventory', dest='inventory_id', type='int',
|
||||||
|
default=0, help='Inventory ID (can also be specified '
|
||||||
|
'using INVENTORY_ID environment variable)')
|
||||||
|
parser.add_option('--list', action='store_true', dest='list',
|
||||||
|
default=False, help='Return JSON hash of host groups.')
|
||||||
|
parser.add_option('--host', dest='hostname', default='',
|
||||||
|
help='Return JSON hash of host vars.')
|
||||||
|
parser.add_option('--indent', dest='indent', type='int', default=None,
|
||||||
|
help='Indentation level for pretty printing output')
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
InventoryScript(**vars(options)).run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -133,7 +133,6 @@ INSTALLED_APPS = (
|
|||||||
INTERNAL_IPS = ('127.0.0.1',)
|
INTERNAL_IPS = ('127.0.0.1',)
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'FILTER_BACKEND': 'ansibleworks.main.custom_filters.CustomFilterBackend',
|
|
||||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'ansibleworks.main.pagination.PaginationSerializer',
|
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'ansibleworks.main.pagination.PaginationSerializer',
|
||||||
'PAGINATE_BY': 25,
|
'PAGINATE_BY': 25,
|
||||||
'PAGINATE_BY_PARAM': 'page_size',
|
'PAGINATE_BY_PARAM': 'page_size',
|
||||||
@@ -142,6 +141,9 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
),
|
),
|
||||||
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
|
'ansibleworks.main.custom_filters.CustomFilterBackend',
|
||||||
|
),
|
||||||
'DEFAULT_PARSER_CLASSES': (
|
'DEFAULT_PARSER_CLASSES': (
|
||||||
'rest_framework.parsers.JSONParser',
|
'rest_framework.parsers.JSONParser',
|
||||||
'rest_framework.parsers.FormParser',
|
'rest_framework.parsers.FormParser',
|
||||||
@@ -218,6 +220,9 @@ DEVSERVER_MODULES = (
|
|||||||
#'devserver.modules.profile.LineProfilerModule',
|
#'devserver.modules.profile.LineProfilerModule',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set default ports for live server tests.
|
||||||
|
os.environ.setdefault('DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:9013-9199')
|
||||||
|
|
||||||
# Skip migrations when running tests.
|
# Skip migrations when running tests.
|
||||||
SOUTH_TESTS_MIGRATE = False
|
SOUTH_TESTS_MIGRATE = False
|
||||||
|
|
||||||
@@ -234,6 +239,11 @@ CELERYD_TASK_SOFT_TIME_LIMIT = 3540
|
|||||||
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
|
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
|
||||||
CELERYBEAT_MAX_LOOP_INTERVAL = 60
|
CELERYBEAT_MAX_LOOP_INTERVAL = 60
|
||||||
|
|
||||||
|
if 'devserver' in INSTALLED_APPS:
|
||||||
|
INTERNAL_API_URL = 'http://127.0.0.1:%s' % DEVSERVER_DEFAULT_PORT
|
||||||
|
else:
|
||||||
|
INTERNAL_API_URL = 'http://127.0.0.1:8000'
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'disable_existing_loggers': False,
|
'disable_existing_loggers': False,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ ALLOWED_HOSTS = []
|
|||||||
# Production should only use minified JS for UI.
|
# Production should only use minified JS for UI.
|
||||||
USE_MINIFIED_JS = True
|
USE_MINIFIED_JS = True
|
||||||
|
|
||||||
|
INTERNAL_API_URL = 'http://127.0.0.1:80'
|
||||||
|
|
||||||
# If a local_settings.py file is present here, use it and ignore the global
|
# If a local_settings.py file is present here, use it and ignore the global
|
||||||
# settings. Normally, local settings would only be present during development.
|
# settings. Normally, local settings would only be present during development.
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ djangorestframework>=2.3.0,<2.4.0
|
|||||||
Markdown
|
Markdown
|
||||||
pexpect
|
pexpect
|
||||||
python-dateutil
|
python-dateutil
|
||||||
|
requests
|
||||||
South>=0.8,<2.0
|
South>=0.8,<2.0
|
||||||
|
|
||||||
django-debug-toolbar
|
django-debug-toolbar
|
||||||
|
|||||||
BIN
requirements/requests-1.2.3.tar.gz
Normal file
BIN
requirements/requests-1.2.3.tar.gz
Normal file
Binary file not shown.
3
setup.py
3
setup.py
@@ -53,7 +53,7 @@ def proc_data_files(data_files):
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='ansibleworks',
|
name='ansibleworks',
|
||||||
version=__version__.split("-")[0],
|
version=__version__.split("-")[0], # FIXME: Should keep full version here?
|
||||||
author='AnsibleWorks, Inc.',
|
author='AnsibleWorks, Inc.',
|
||||||
author_email='support@ansibleworks.com',
|
author_email='support@ansibleworks.com',
|
||||||
description='AnsibleWorks API, UI and Task Engine',
|
description='AnsibleWorks API, UI and Task Engine',
|
||||||
@@ -75,6 +75,7 @@ setup(
|
|||||||
'pexpect',
|
'pexpect',
|
||||||
'python-dateutil',
|
'python-dateutil',
|
||||||
'PyYAML',
|
'PyYAML',
|
||||||
|
'requests',
|
||||||
'South>=0.8,<2.0',
|
'South>=0.8,<2.0',
|
||||||
],
|
],
|
||||||
setup_requires=[],
|
setup_requires=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user