Adding tower_api and tower_get_id lookup plugins

This commit is contained in:
John Westcott IV
2020-06-08 13:20:32 -04:00
parent 09dcb91c09
commit 6d626b3793
6 changed files with 403 additions and 25 deletions

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Wayne Witzel III <wayne@riotousliving.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Ansible Tower documentation fragment
DOCUMENTATION = r'''
options:
host:
description: The network address of your Ansible Tower host.
env:
- name: TOWER_HOST
username:
description: The user that you plan to use to access inventories on Ansible Tower.
env:
- name: TOWER_USERNAME
password:
description: The password for your Ansible Tower user.
env:
- name: TOWER_PASSWORD
oauth_token:
description:
- The Tower OAuth token to use.
env:
- name: TOWER_OAUTH_TOKEN
verify_ssl:
description:
- Specify whether Ansible should verify the SSL certificate of Ansible Tower host.
- Defaults to True, but this is handled by the shared module_utils code
type: bool
env:
- name: TOWER_VERIFY_SSL
aliases: [ validate_certs ]
notes:
- If no I(config_file) is provided we will attempt to use the tower-cli library
defaults to find your Tower host information.
- I(config_file) should contain Tower configuration in the following format
host=hostname
username=username
password=password
'''

View File

@@ -19,24 +19,9 @@ DOCUMENTATION = '''
the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file
are missing, this plugin will try to fill in missing arguments by reading from environment variables. are missing, this plugin will try to fill in missing arguments by reading from environment variables.
- If reading configurations from environment variables, the path in the command must be @tower_inventory. - If reading configurations from environment variables, the path in the command must be @tower_inventory.
extends_documentation_fragment:
- awx.awx.auth_plugin
options: options:
host:
description: The network address of your Ansible Tower host.
env:
- name: TOWER_HOST
username:
description: The user that you plan to use to access inventories on Ansible Tower.
env:
- name: TOWER_USERNAME
password:
description: The password for your Ansible Tower user.
env:
- name: TOWER_PASSWORD
oauth_token:
description:
- The Tower OAuth token to use.
env:
- name: TOWER_OAUTH_TOKEN
inventory_id: inventory_id:
description: description:
- The ID of the Ansible Tower inventory that you wish to import. - The ID of the Ansible Tower inventory that you wish to import.
@@ -47,14 +32,6 @@ DOCUMENTATION = '''
env: env:
- name: TOWER_INVENTORY - name: TOWER_INVENTORY
required: True required: True
verify_ssl:
description:
- Specify whether Ansible should verify the SSL certificate of Ansible Tower host.
- Defaults to True, but this is handled by the shared module_utils code
type: bool
env:
- name: TOWER_VERIFY_SSL
aliases: [ validate_certs ]
include_metadata: include_metadata:
description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
type: bool type: bool

View File

@@ -0,0 +1,110 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
lookup: tower_api
author: John Westcott IV (@john-westcott-iv)
short_description: Search the API for objects
requirements:
- None
description:
- Returns GET requests to the Ansible Tower API. See
U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/index.html) for API usage.
extends_documentation_fragment:
- awx.awx.auth_plugin
options:
_terms:
description:
- The endpoint to query. i.e. teams, users, tokens, job_templates, etc
required: True
query_params:
description:
- The query parameters to search for in the form of key/value pairs.
type: dict
required: True
get_all:
description:
- If the resulting query is pagenated, retriest all pages
- note: If the query is not filtered properly this can cause a performance impact
type: boolean
default: False
"""
EXAMPLES = """
- name: Lookup any users who are admins
debug:
msg: "{{ query('awx.awx.tower_api', 'users', query_params={ 'is_superuser': true }) }}"
"""
RETURN = """
_raw:
description:
- Response of objects from API
type: dict
contains:
count:
description: The number of objects your filter returned in total (not necessarally on this page)
type: str
next:
description: The URL path for the next page
type: str
previous:
description: The URL path for the previous page
type: str
results:
description: An array of results that were returned
type: list
returned: on successful create
"""
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.utils.display import Display
from ..module_utils.tower_api import TowerModule
class LookupModule(LookupBase):
display = Display()
def handle_error(self, **kwargs):
raise AnsibleError(to_native(kwargs.get('msg')))
def warn_callback(self, warning):
self.display.warning(warning)
def run(self, terms, variables=None, **kwargs):
if len(terms) != 1:
raise AnsibleError('You must pass exactly one endpoint to query')
# Defer processing of params to logic shared with the modules
module_params = {}
for plugin_param, module_param in TowerModule.short_params.items():
opt_val = self.get_option(plugin_param)
if opt_val is not None:
module_params[module_param] = opt_val
# Create our module
module = TowerModule(
argument_spec={}, direct_params=module_params,
error_callback=self.handle_error, warn_callback=self.warn_callback
)
self.set_options(direct=kwargs)
if self.get_option('get_all'):
return_data = module.get_all_endpoint(terms[0], data=self.get_option('query_params'))
else:
return_data = module.get_endpoint(terms[0], data=self.get_option('query_params'))
with open('/tmp/john', 'w') as f:
import json
f.write(json.dumps(return_data, indent=4))
if return_data['status_code'] != 200:
error = return_data
if return_data.get('json', {}).get('detail', False):
error = return_data['json']['detail']
raise AnsibleError("Failed to query the API: {0}".format(error))
return return_data['json']

View File

@@ -0,0 +1,81 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
lookup: tower_get_id
author: John Westcott IV (@john-westcott-iv)
short_description: Search for a specific ID of an option
requirements:
- None
description:
- Returns an ID of an object found in tower by the fiter criteria. See
U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/index.html) for API usage.
Raises an exception if not exactly one object is found.
extends_documentation_fragment:
- awx.awx.auth_plugin
options:
_terms:
description:
- The endpoint to query. i.e. teams, users, tokens, job_templates, etc
required: True
query_params:
description:
- The query parameters to search for in the form of key/value pairs.
type: dict
required: True
"""
EXAMPLES = """
- name: Lookup a users ID
debug:
msg: "{{ query('awx.awx.tower_api', 'users', query_params={ 'username': 'admin' }) }}"
"""
RETURN = """
_raw:
description:
- The ID found for the filter criteria
type: str
"""
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.utils.display import Display
from ..module_utils.tower_api import TowerModule
class LookupModule(LookupBase):
display = Display()
def handle_error(self, **kwargs):
raise AnsibleError(to_native(kwargs.get('msg')))
def warn_callback(self, warning):
self.display.warning(warning)
def run(self, terms, variables=None, **kwargs):
if len(terms) != 1:
raise AnsibleError('You must pass exactly one endpoint to query')
# Defer processing of params to logic shared with the modules
module_params = {}
for plugin_param, module_param in TowerModule.short_params.items():
opt_val = self.get_option(plugin_param)
if opt_val is not None:
module_params[module_param] = opt_val
# Create our module
module = TowerModule(
argument_spec={}, direct_params=module_params,
error_callback=self.handle_error, warn_callback=self.warn_callback
)
self.set_options(direct=kwargs)
found_object = module.get_one(terms[0], data=self.get_option('query_params'))
if found_object is None:
self.handle_error(msg='No objects matched that criteria')
else:
return found_object['id']

View File

@@ -0,0 +1,84 @@
---
- name: Generate a random string for test
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate usernames
set_fact:
usernames:
- "AWX-Collection-tests-tower_api_lookup-user1-{{ test_id }}"
- "AWX-Collection-tests-tower_api_lookup-user2-{{ test_id }}"
- "AWX-Collection-tests-tower_api_lookup-user3-{{ test_id }}"
- name: Create all of our users
tower_user:
username: "{{ item }}"
is_superuser: true
password: "{{ test_id }}"
loop: "{{ usernames }}"
register: user_creation_results
- block:
- name: Test too many params (failure from validation of terms)
set_fact:
junk: "{{ query('awx.awx.tower_api', 'users', 'teams', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'ou must pass exactly one endpoint to query' in result.msg"
- name: Try to load invalid endpoint
set_fact:
junk: "{{ query('awx.awx.tower_api', 'john', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'The requested object could not be found at' in result.msg"
- name: Load user of a specific name
set_fact:
users: "{{ query('awx.awx.tower_api', 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }) }}"
- assert:
that:
- users['results'] | length() == 1
- users['count'] == 1
- name: Get the id of the admin users
set_fact:
user_id: "{{ (query('awx.awx.tower_api', 'users', query_params=query_params) | json_query(jmes_query))[0] }}"
vars:
query_params:
username: "{{ user_creation_results['results'][0]['item'] }}"
jmes_query: 'results[*].id'
- assert:
that: "{{ user_id }} == {{ user_creation_results['results'][0]['id'] }}"
- name: Get a page of users
set_fact:
users: "{{ query('awx.awx.tower_api', 'users', query_params={ 'page_size': 2 } ) }}"
- assert:
that: users['results'] | length() == 2
- name: Get all users of a system through next attribute
set_fact:
users: "{{ query('awx.awx.tower_api', 'users', query_params={ 'page_size': 1, }, get_all=true ) }}"
- assert:
that: users['results'] | length() >= 3
always:
- name: Cleanup users
tower_user:
username: "{{ item }}"
state: absent
loop: "{{ usernames }}"

View File

@@ -0,0 +1,78 @@
---
- name: Generate a random string for test
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate usernames
set_fact:
usernames:
- "AWX-Collection-tests-tower_get_id-user1-{{ test_id }}"
- "AWX-Collection-tests-tower_get_id-user2-{{ test_id }}"
- "AWX-Collection-tests-tower_get_id-user3-{{ test_id }}"
- name: Create all of our users
tower_user:
username: "{{ item }}"
is_superuser: true
password: "{{ test_id }}"
loop: "{{ usernames }}"
register: user_creation_results
- block:
- name: Test too many params (failure from validation of terms)
debug:
msg: "{{ query('awx.awx.tower_get_id', 'users', 'teams', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'ou must pass exactly one endpoint to query' in result.msg"
- name: Try to load invalid endpoint
debug:
msg: "{{ query('awx.awx.tower_get_id', 'john', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'The requested object could not be found at' in result.msg"
- name: Get the ID of the admin user
set_fact:
user_id: "{{ query('awx.awx.tower_get_id', 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }) }}"
- assert:
that: "{{ user_id }} == {{ user_creation_results['results'][0]['id'] }}"
- name: Try to get an ID of someone who does not exist
set_fact:
failed_user_id: "{{ query('awx.awx.tower_get_id', 'users', query_params={ 'username': 'john jacob jingleheimer schmidt' }) }}"
register: results
ignore_errors: true
- assert:
that:
- results is failed
- "'No objects matched that criteria' in results['msg']"
- name: Lookup too many users
set_fact:
too_many_user_ids: " {{ query('awx.awx.tower_get_id', 'users', query_params={ 'username__startswith': 'AWX-Collection-tests-tower_get_id-' }) }}"
register: results
ignore_errors: True
- assert:
that:
- results is failed
- "'An unexpected number of items was returned from the API (3)' in results['msg']"
always:
- name: Cleanup users
tower_user:
username: "{{ item }}"
state: absent
loop: "{{ usernames }}"