For AC-332. Added support to inventory script view, inventory script and task engine to include hostvars inline when using Ansible >= 1.3.

This commit is contained in:
Chris Church
2013-08-27 00:08:54 -04:00
parent 2bb5374685
commit 0129036b40
7 changed files with 164 additions and 4 deletions

View File

@@ -3,6 +3,7 @@
# Python # Python
import cStringIO import cStringIO
import distutils.version
import json import json
import logging import logging
import os import os
@@ -23,6 +24,7 @@ from django.conf import settings
# AWX # AWX
from awx.main.models import Job, ProjectUpdate from awx.main.models import Job, ProjectUpdate
from awx.main.utils import get_ansible_version
__all__ = ['RunJob', 'RunProjectUpdate'] __all__ = ['RunJob', 'RunProjectUpdate']
@@ -228,6 +230,16 @@ class RunJob(BaseTask):
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_URL'] = settings.INTERNAL_API_URL
env['REST_API_TOKEN'] = job.task_auth_token or '' env['REST_API_TOKEN'] = job.task_auth_token or ''
# When using Ansible >= 1.3, allow the inventory script to include host
# variables inline via ['_meta']['hostvars'].
try:
Version = distutils.version.StrictVersion
if Version( get_ansible_version()) >= Version('1.3'):
env['INVENTORY_HOSTVARS'] = str(True)
except ValueError:
pass
return env return env
def build_args(self, job, **kwargs): def build_args(self, job, **kwargs):

View File

@@ -1,10 +1,25 @@
Generate inventory group and host data as needed for an inventory script. Generate inventory group and host data as needed for an inventory script.
Refer to [External Inventory Scripts](http://www.ansibleworks.com/docs/api.html#external-inventory-scripts)
for more information on inventory scripts.
## List Response
Make a GET request to this resource without query parameters to retrieve a JSON Make a GET request to this resource without query parameters to retrieve a JSON
object containing groups, including the hosts, children and variables for each object containing groups, including the hosts, children and variables for each
group. The response data is equivalent to that returned by passing the group. The response data is equivalent to that returned by passing the
`--list` argument to an inventory script. `--list` argument to an inventory script.
_(New in AWX 1.3)_ Specify a query string of `?hostvars=1` to retrieve the JSON
object above including all host variables. The `['_meta']['hostvars']` object
in the response contains an entry for each host with its variables. This
response format can be used with Ansible 1.3 and later to avoid making a
separate API request for each host. Refer to
[Tuning the External Inventory Script](http://www.ansibleworks.com/docs/api.html#tuning-the-external-inventory-script)
for more information on this feature.
## Host Response
Make a GET request to this resource with a query string similar to Make a GET request to this resource with a query string similar to
`?host=HOSTNAME` to retrieve a JSON object containing host variables for the `?host=HOSTNAME` to retrieve a JSON object containing host variables for the
specified host. The response data is equivalent to that returned by passing specified host. The response data is equivalent to that returned by passing

View File

@@ -663,7 +663,60 @@ class InventoryTest(BaseTest):
# on a group resource, I can see related resources for variables, inventories, and children # on a group resource, I can see related resources for variables, inventories, and children
# and these work # and these work
def test_get_inventory_tree(self): def test_get_inventory_script_view(self):
i_a = self.inventory_a
i_a.variables = json.dumps({'i-vars': 123})
i_a.save()
# Group A is parent of B, B is parent of C, C is parent of D.
g_a = i_a.groups.create(name='A', variables=json.dumps({'A-vars': 'AAA'}))
g_b = i_a.groups.create(name='B', variables=json.dumps({'B-vars': 'BBB'}))
g_b.parents.add(g_a)
g_c = i_a.groups.create(name='C', variables=json.dumps({'C-vars': 'CCC'}))
g_c.parents.add(g_b)
g_d = i_a.groups.create(name='D', variables=json.dumps({'D-vars': 'DDD'}))
g_d.parents.add(g_c)
# Each group "X" contains one host "x".
h_a = i_a.hosts.create(name='a', variables=json.dumps({'a-vars': 'aaa'}))
h_a.groups.add(g_a)
h_b = i_a.hosts.create(name='b', variables=json.dumps({'b-vars': 'bbb'}))
h_b.groups.add(g_b)
h_c = i_a.hosts.create(name='c', variables=json.dumps({'c-vars': 'ccc'}))
h_c.groups.add(g_c)
h_d = i_a.hosts.create(name='d', variables=json.dumps({'d-vars': 'ddd'}))
h_d.groups.add(g_d)
# Old, slow 1.2 way.
url = reverse('main:inventory_script_view', args=(i_a.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue('all' in response)
self.assertEqual(response['all']['vars'], i_a.variables_dict)
for g in i_a.groups.all():
self.assertTrue(g.name in response)
self.assertEqual(response[g.name]['vars'], g.variables_dict)
self.assertEqual(set(response[g.name]['children']),
set(g.children.values_list('name', flat=True)))
self.assertEqual(set(response[g.name]['hosts']),
set(g.hosts.values_list('name', flat=True)))
self.assertFalse('_meta' in response)
for h in i_a.hosts.all():
h_url = '%s?host=%s' % (url, h.name)
with self.current_user(self.super_django_user):
response = self.get(h_url, expect=200)
self.assertEqual(response, h.variables_dict)
# New 1.3 way.
url = reverse('main:inventory_script_view', args=(i_a.pk,))
url = '%s?hostvars=1' % url
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue('_meta' in response)
self.assertTrue('hostvars' in response['_meta'])
for h in i_a.hosts.all():
self.assertEqual(response['_meta']['hostvars'][h.name],
h.variables_dict)
def test_get_inventory_tree_view(self):
# Group A is parent of B, B is parent of C, C is parent of D. # Group A is parent of B, B is parent of C, C is parent of D.
g_a = self.inventory_a.groups.create(name='A') g_a = self.inventory_a.groups.create(name='A')
g_b = self.inventory_a.groups.create(name='B') g_b = self.inventory_a.groups.create(name='B')

View File

@@ -208,6 +208,55 @@ class InventoryScriptTest(BaseScriptTest):
else: else:
self.assertTrue(len(v['children']) == 0) self.assertTrue(len(v['children']) == 0)
def test_list_with_hostvars_inline(self):
inventory = self.inventories[1]
self.assertTrue(inventory.active)
rc, stdout, stderr = self.run_inventory_script(list=True,
inventory=inventory.pk,
hostvars=True)
self.assertEqual(rc, 0, stderr)
data = json.loads(stdout)
groups = inventory.groups.filter(active=True)
groupnames = list(groups.values_list('name', flat=True))
groupnames.extend(['all', '_meta'])
self.assertEqual(set(data.keys()), set(groupnames))
all_hostnames = set()
# 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
if k == '_meta':
continue
group = inventory.groups.get(active=True, name=k)
hosts = group.hosts.filter(active=True)
hostnames = hosts.values_list('name', flat=True)
all_hostnames.update(hostnames)
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.assertTrue(len(v['children']) == 0)
# Check hostvars in ['_meta']['hostvars'] dict.
for hostname in all_hostnames:
self.assertTrue(hostname in data['_meta']['hostvars'])
host = inventory.hosts.get(name=hostname)
self.assertEqual(data['_meta']['hostvars'][hostname],
host.variables_dict)
# Hostvars can also be requested via environment variable.
os.environ['INVENTORY_HOSTVARS'] = str(True)
rc, stdout, stderr = self.run_inventory_script(list=True,
inventory=inventory.pk)
self.assertEqual(rc, 0, stderr)
data = json.loads(stdout)
self.assertTrue('_meta' in data)
def test_valid_host(self): def test_valid_host(self):
# Host without variable data. # Host without variable data.
inventory = self.inventories[0] inventory = self.inventories[0]
@@ -280,11 +329,10 @@ class InventoryScriptTest(BaseScriptTest):
self.assertNotEqual(rc, 0, stderr) self.assertNotEqual(rc, 0, stderr)
self.assertEqual(json.loads(stdout), {}) self.assertEqual(json.loads(stdout), {})
def _test_with_both_list_and_host_arguments(self): def test_with_both_list_and_host_arguments(self):
inventory = self.inventories[0] inventory = self.inventories[0]
self.assertTrue(inventory.active) self.assertTrue(inventory.active)
os.environ['INVENTORY_ID'] = str(inventory.pk) os.environ['INVENTORY_ID'] = str(inventory.pk)
rc, stdout, stderr = self.run_inventory_script(list=True, host='blah') rc, stdout, stderr = self.run_inventory_script(list=True, host='blah')
self.assertNotEqual(rc, 0, stderr) self.assertNotEqual(rc, 0, stderr)
self.assertEqual(json.loads(stdout), {}) self.assertEqual(json.loads(stdout), {})

View File

@@ -1,13 +1,17 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Python # Python
import logging import logging
import re import re
import subprocess
import sys import sys
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied from rest_framework.exceptions import ParseError, PermissionDenied
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
'get_awx_version'] 'get_ansible_version', 'get_awx_version']
def get_object_or_400(klass, *args, **kwargs): def get_object_or_400(klass, *args, **kwargs):
''' '''
@@ -53,6 +57,18 @@ class RequireDebugTrueOrTest(logging.Filter):
from django.conf import settings from django.conf import settings
return settings.DEBUG or 'test' in sys.argv return settings.DEBUG or 'test' in sys.argv
def get_ansible_version():
'''
Return Ansible version installed.
'''
try:
proc = subprocess.Popen(['ansible', '--version'],
stdout=subprocess.PIPE)
result = proc.communicate()[0]
return result.lower().replace('ansible', '').strip()
except:
return 'unknown'
def get_awx_version(): def get_awx_version():
''' '''
Return AWX version as reported by setuptools. Return AWX version as reported by setuptools.

View File

@@ -111,6 +111,7 @@ class ApiV1ConfigView(APIView):
time_zone=settings.TIME_ZONE, time_zone=settings.TIME_ZONE,
license_info=license_data, license_info=license_data,
version=get_awx_version(), version=get_awx_version(),
ansible_version=get_ansible_version(),
) )
if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count(): if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count():
data.update(dict( data.update(dict(
@@ -584,6 +585,7 @@ class InventoryScriptView(RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
hostname = request.QUERY_PARAMS.get('host', '') hostname = request.QUERY_PARAMS.get('host', '')
hostvars = bool(request.QUERY_PARAMS.get('hostvars', ''))
if hostname: if hostname:
host = get_object_or_404(self.object.hosts, active=True, host = get_object_or_404(self.object.hosts, active=True,
name=hostname) name=hostname)
@@ -603,6 +605,12 @@ class InventoryScriptView(RetrieveAPIView):
group_info['vars'] = group.variables_dict group_info['vars'] = group.variables_dict
data[group.name] = group_info data[group.name] = group_info
if hostvars:
data.setdefault('_meta', SortedDict())
data['_meta'].setdefault('hostvars', SortedDict())
for host in self.object.hosts.filter(active=True):
data['_meta']['hostvars'][host.name] = host.variables_dict
# workaround for Ansible inventory bug (github #3687), localhost # workaround for Ansible inventory bug (github #3687), localhost
# must be explicitly listed in the all group for dynamic inventory # must be explicitly listed in the all group for dynamic inventory
# scripts to pick it up. # scripts to pick it up.

View File

@@ -78,6 +78,8 @@ class InventoryScript(object):
url_path = '/api/v1/inventories/%d/script/' % self.inventory_id url_path = '/api/v1/inventories/%d/script/' % self.inventory_id
if self.hostname: if self.hostname:
url_path += '?%s' % urllib.urlencode({'host': self.hostname}) url_path += '?%s' % urllib.urlencode({'host': self.hostname})
elif self.hostvars:
url_path += '?%s' % urllib.urlencode({'hostvars': 1})
url = urlparse.urljoin(url, url_path) url = urlparse.urljoin(url, url_path)
response = requests.get(url, auth=auth) response = requests.get(url, auth=auth)
response.raise_for_status() response.raise_for_status()
@@ -107,6 +109,8 @@ class InventoryScript(object):
raise ValueError('No inventory ID specified') raise ValueError('No inventory ID specified')
self.hostname = self.options.get('hostname', '') self.hostname = self.options.get('hostname', '')
self.list_ = self.options.get('list', False) self.list_ = self.options.get('list', False)
self.hostvars = bool(self.options.get('hostvars', False) or
os.getenv('INVENTORY_HOSTVARS', ''))
self.indent = self.options.get('indent', None) self.indent = self.options.get('indent', None)
if self.list_ and self.hostname: if self.list_ and self.hostname:
raise RuntimeError('Only --list or --host may be specified') raise RuntimeError('Only --list or --host may be specified')
@@ -147,6 +151,10 @@ def main():
'using INVENTORY_ID environment variable)') 'using INVENTORY_ID environment variable)')
parser.add_option('--list', action='store_true', dest='list', parser.add_option('--list', action='store_true', dest='list',
default=False, help='Return JSON hash of host groups.') default=False, help='Return JSON hash of host groups.')
parser.add_option('--hostvars', action='store_true', dest='hostvars',
default=False, help='Return hostvars inline with --list,'
' under ["_meta"]["hostvars"]. Can also be specified '
'using INVENTORY_HOSTVARS environment variable.')
parser.add_option('--host', dest='hostname', default='', parser.add_option('--host', dest='hostname', default='',
help='Return JSON hash of host vars.') help='Return JSON hash of host vars.')
parser.add_option('--indent', dest='indent', type='int', default=None, parser.add_option('--indent', dest='indent', type='int', default=None,