Using an Ansible Tower Inventory from Command Line Ansible

In an earlier post, I wrote about using the OpenStack Ansible inventory helper when calling and Ansible command line tools. However, When developing an playbook, often there is more information pulled from the inventory than just the set of hosts. Often, the inventory also collects variables that are used in common across multiple playbooks. For this reason, and many more, I want to be able to call an Ansible playbook or Ad-Hoc command from the command line, but use the inventory as defined by an Ansible Tower instance. It turns out this is fairly simple to do, using the REST API.

If you have a working Ansible Tower instance, you can find the REST API starting point by editing the URL in your browser. For example, I have a VM running on my machine listening on local IP address 192.168.122.151. If, after authenticating, I edit the URL to https://192.168.122.151/api/ I see the page below:

To navigate to this same page via curl, I run:

curl -s -k https://192.168.122.151/api/

Which returns:

{"available_versions":{"v1":"/api/v1/","v2":"/api/v2/"},"description":"AWX REST API","current_version":"/api/v2/"}

To go much deeper into the API I need to be authenticated.  I can do that here using Basic-Auth, and a little jq to make it legible:

 export CREDS='admin:password'
 
$ curl -s -k -u $CREDS https://192.168.122.151/api/v2/ | jq '

Which returns

{
"authtoken": "/api/v2/authtoken/",
"ping": "/api/v2/ping/",
"instances": "/api/v2/instances/",
"instance_groups": "/api/v2/instance_groups/",
"config": "/api/v2/config/",
"settings": "/api/v2/settings/",
"me": "/api/v2/me/",
"dashboard": "/api/v2/dashboard/",
"organizations": "/api/v2/organizations/",
"users": "/api/v2/users/",
"projects": "/api/v2/projects/",
"project_updates": "/api/v2/project_updates/",
"teams": "/api/v2/teams/",
"credentials": "/api/v2/credentials/",
"credential_types": "/api/v2/credential_types/",
"inventory": "/api/v2/inventories/",
"inventory_scripts": "/api/v2/inventory_scripts/",
"inventory_sources": "/api/v2/inventory_sources/",
"inventory_updates": "/api/v2/inventory_updates/",
"groups": "/api/v2/groups/",
"hosts": "/api/v2/hosts/",
"job_templates": "/api/v2/job_templates/",
"jobs": "/api/v2/jobs/",
"job_events": "/api/v2/job_events/",
"ad_hoc_commands": "/api/v2/ad_hoc_commands/",
"system_job_templates": "/api/v2/system_job_templates/",
"system_jobs": "/api/v2/system_jobs/",
"schedules": "/api/v2/schedules/",
"roles": "/api/v2/roles/",
"notification_templates": "/api/v2/notification_templates/",
"notifications": "/api/v2/notifications/",
"labels": "/api/v2/labels/",
"unified_job_templates": "/api/v2/unified_job_templates/",
"unified_jobs": "/api/v2/unified_jobs/",
"activity_stream": "/api/v2/activity_stream/",
"workflow_job_templates": "/api/v2/workflow_job_templates/",
"workflow_jobs": "/api/v2/workflow_jobs/",
"workflow_job_template_nodes": "/api/v2/workflow_job_template_nodes/",
"workflow_job_nodes": "/api/v2/workflow_job_nodes/"
}

This call lays out all of the top level objects available in tower. The “inventory” suburl, “/api/v2/inventories/,” enumerates the inventory objects recorded in the system. Because there is a lot of information available on each object, I’ve found it necessary to filter down by name:

 $ curl -s -k -u $CREDS https://192.168.122.151/api/v2/inventories/ | jq '.results | .[] | .name'
"Demo Inventory"
"Generic OCP Inventory"
"Ibis"
"MOC_RedHatFSI"
"RDU-OpenStack"

And to get access to a specific inventory:

 curl -s -k -u $CREDS https://192.168.122.151/api/v2/inventories/ | jq '.results | .[] | select (.name == "MOC_RedHatFSI") '

Again, returning a lot of information. What I care about, however, is the URL that I can call to create an inventory json structure consumable by Ansible. And this is in the related collection in the field script.

 
$ curl -s -k -u $CREDS https://192.168.122.151/api/v2/inventories/ | jq '.results | .[] | select (.name == "MOC_RedHatFSI") |.related '
{
"created_by": "/api/v2/users/1/",
"job_templates": "/api/v2/inventories/4/job_templates/",
"variable_data": "/api/v2/inventories/4/variable_data/",
"root_groups": "/api/v2/inventories/4/root_groups/",
"object_roles": "/api/v2/inventories/4/object_roles/",
"ad_hoc_commands": "/api/v2/inventories/4/ad_hoc_commands/",
"script": "/api/v2/inventories/4/script/",
"tree": "/api/v2/inventories/4/tree/",
"access_list": "/api/v2/inventories/4/access_list/",
"activity_stream": "/api/v2/inventories/4/activity_stream/",
"instance_groups": "/api/v2/inventories/4/instance_groups/",
"hosts": "/api/v2/inventories/4/hosts/",
"groups": "/api/v2/inventories/4/groups/",
"update_inventory_sources": "/api/v2/inventories/4/update_inventory_sources/",
"inventory_sources": "/api/v2/inventories/4/inventory_sources/",
"organization": "/api/v2/organizations/1/"
}

Peeking at the source code for AWX shows just how it uses this field in when one Tower instance acts as the inventory for another.

That lead me to write a short script that I can call as part of the Ansible command line:

$ cat ~/bin/tower-inventory.sh
#!/bin/sh
export TOWER_CREDS='admin:password'
export TOWER_HOST=192.168.122.151
export INVNUM=4
 
curl  -s -k  -u $TOWER_CREDS https://$TOWER_HOST/api/v2/inventories/$INVNUM/script/?hostvars=1&towervars=1&all=1

Which I can then use as below:

$ ansible --key-file ~/keys/id_rsa  --user cloud-user   -i ~/bin/tower-inventory.sh idm -m shell -a 'echo $USER'
idm | SUCCESS | rc=0 >>
cloud-user

This should be sufficient to get you started. This script can be more flexible. Aside from the obvious changes to consume external values for the variables, it could mimic the work I performed earlier in the article to navigate down, and find an inventory by name. However, that would also slow down the processing, as there would be several more round trip calls to tower.

Leave a Reply

Your email address will not be published. Required fields are marked *