Merge pull request #7286 from john-westcott-iv/lookup_plugins

Adding tower_api and tower_get_id lookup plugins

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-07-14 18:04:27 +00:00
committed by GitHub
7 changed files with 614 additions and 61 deletions

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible by Red Hat, Inc
# 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

@@ -6,59 +6,35 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
name: tower name: tower
plugin_type: inventory plugin_type: inventory
author: author:
- Matthew Jones (@matburt) - Matthew Jones (@matburt)
- Yunfan Zhang (@YunfanZhang42) - Yunfan Zhang (@YunfanZhang42)
short_description: Ansible dynamic inventory plugin for Ansible Tower. short_description: Ansible dynamic inventory plugin for Ansible Tower.
description: description:
- Reads inventories from Ansible Tower. - Reads inventories from Ansible Tower.
- Supports reading configuration from both YAML config file and environment variables. - Supports reading configuration from both YAML config file and environment variables.
- If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml), - If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml),
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.
options: extends_documentation_fragment: awx.awx.auth_plugin
host: options:
description: The network address of your Ansible Tower host. inventory_id:
env: description:
- name: TOWER_HOST - The ID of the Ansible Tower inventory that you wish to import.
username: - This is allowed to be either the inventory primary key or its named URL slug.
description: The user that you plan to use to access inventories on Ansible Tower. - Primary key values will be accepted as strings or integers, and URL slugs must be strings.
env: - Named URL slugs follow the syntax of "inventory_name++organization_name".
- name: TOWER_USERNAME type: raw
password: env:
description: The password for your Ansible Tower user. - name: TOWER_INVENTORY
env: required: True
- name: TOWER_PASSWORD include_metadata:
oauth_token: description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
description: type: bool
- The Tower OAuth token to use. default: False
env:
- name: TOWER_OAUTH_TOKEN
inventory_id:
description:
- The ID of the Ansible Tower inventory that you wish to import.
- This is allowed to be either the inventory primary key or its named URL slug.
- Primary key values will be accepted as strings or integers, and URL slugs must be strings.
- Named URL slugs follow the syntax of "inventory_name++organization_name".
type: raw
env:
- name: TOWER_INVENTORY
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:
description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
type: bool
default: False
''' '''
EXAMPLES = ''' EXAMPLES = '''

View File

@@ -0,0 +1,196 @@
# (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 from the Ansible Tower API. See
U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/index.html) for API usage.
- For use that is cross-compatible between the awx.awx and ansible.tower collection
see the tower_meta module
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: False
aliases: [query, data, filter, params]
expect_objects:
description:
- Error if the response does not contain either a detail view or a list view.
type: boolean
default: False
aliases: [expect_object]
expect_one:
description:
- Error if the response contains more than one object.
type: boolean
default: False
return_objects:
description:
- If a list view is returned, promote the list of results to the top-level of list returned.
- Allows using this lookup plugin to loop over objects without additional work.
type: boolean
default: True
return_all:
description:
- If the response is paginated, return all pages.
type: boolean
default: False
return_ids:
description:
- If response contains objects, promote the id key to the top-level entries in the list.
- Allows looking up a related object and passing it as a parameter to another module.
- This will convert the return to a string or list of strings depending on the number of selected items.
type: boolean
aliases: [return_id]
default: False
max_objects:
description:
- if C(return_all) is true, this is the maximum of number of objects to return from the list.
- If a list view returns more an max_objects an exception will be raised
type: integer
default: 1000
notes:
- If the query is not filtered properly this can cause a performance impact.
"""
EXAMPLES = """
- name: Load the UI settings
set_fact:
tower_settings: "{{ lookup('awx.awx.tower_api', 'settings/ui') }}"
- name: Report the usernames of all users with admin privs
debug:
msg: "Admin users: {{ query('awx.awx.tower_api', 'users', query_params={ 'is_superuser': true }) | map(attribute='username') | join(', ') }}"
- name: debug all organizations in a loop # use query to return a list
debug:
msg: "Organization description={{ item['description'] }} id={{ item['id'] }}"
loop: "{{ query('awx.awx.tower_api', 'organizations') }}"
loop_control:
label: "{{ item['name'] }}"
- name: Make sure user 'john' is an org admin of the default org if the user exists
tower_role:
organization: Default
role: admin
user: john
when: "lookup('awx.awx.tower_api', 'users', query_params={ 'username': 'john' }) | length == 1"
- name: Create an inventory group with all 'foo' hosts
tower_group:
name: "Foo Group"
inventory: "Demo Inventory"
hosts: >-
{{ query(
'awx.awx.tower_api',
'hosts',
query_params={ 'name__startswith' : 'foo', },
) | map(attribute='name') | list }}
register: group_creation
"""
RETURN = """
_raw:
description:
- Response from the API
type: dict
returned: on successful request
"""
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)
response = module.get_endpoint(terms[0], data=self.get_option('query_params', {}))
if 'status_code' not in response:
raise AnsibleError("Unclear response from API: {0}".format(response))
if response['status_code'] != 200:
raise AnsibleError("Failed to query the API: {0}".format(response['json'].get('detail', response['json'])))
return_data = response['json']
if self.get_option('expect_objects') or self.get_option('expect_one'):
if ('id' not in return_data) and ('results' not in return_data):
raise AnsibleError(
'Did not obtain a list or detail view at {0}, and '
'expect_objects or expect_one is set to True'.format(terms[0])
)
if self.get_option('expect_one'):
if 'results' in return_data and len(return_data['results']) != 1:
raise AnsibleError(
'Expected one object from endpoint {0}, '
'but obtained {1} from API'.format(terms[0], len(return_data['results']))
)
if self.get_option('return_all') and 'results' in return_data:
if return_data['count'] > self.get_option('max_objects'):
raise AnsibleError(
'List view at {0} returned {1} objects, which is more than the maximum allowed '
'by max_objects, {2}'.format(terms[0], return_data['count'], self.get_option('max_objects'))
)
next_page = return_data['next']
while next_page is not None:
next_response = module.get_endpoint(next_page)
return_data['results'] += next_response['json']['results']
next_page = next_response['json']['next']
return_data['next'] = None
if self.get_option('return_ids'):
if 'results' in return_data:
return_data['results'] = [str(item['id']) for item in return_data['results']]
elif 'id' in return_data:
return_data = str(return_data['id'])
if self.get_option('return_objects') and 'results' in return_data:
return return_data['results']
else:
return [return_data]

View File

@@ -0,0 +1,84 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2020, Ansible by Red Hat, Inc
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: tower_meta
author: "Alan Rominger (@alancoding)"
short_description: Returns metadata about the collection this module lives in.
description:
- Allows a user to find out what collection this module exists in.
- This takes common module parameters, but does nothing with them.
options: {}
extends_documentation_fragment: awx.awx.auth
'''
RETURN = '''
prefix:
description: Collection namespace and name in the namespace.name format
returned: success
sample: awx.awx
type: str
name:
description: Collection name
returned: success
sample: awx
type: str
namespace:
description: Collection namespace
returned: success
sample: awx
type: str
version:
description: Version of the collection
returned: success
sample: 0.0.1-devel
type: str
'''
EXAMPLES = '''
- tower_meta:
register: result
- name: Show details about the collection
debug: var=result
- name: Load the UI setting without hard-coding the collection name
debug:
msg: "{{ lookup(result.prefix + '.tower_api', 'settings/ui') }}"
'''
from ..module_utils.tower_api import TowerModule
def main():
module = TowerModule(argument_spec={})
namespace = {
'awx': 'awx',
'tower': 'ansible'
}.get(module._COLLECTION_TYPE, 'unknown')
namespace_name = '{0}.{1}'.format(namespace, module._COLLECTION_TYPE)
module.exit_json(
prefix=namespace_name,
name=module._COLLECTION_TYPE,
namespace=namespace,
version=module._COLLECTION_VERSION
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,238 @@
---
- 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 }}"
hosts:
- "AWX-Collection-tests-tower_api_lookup-host1-{{ test_id }}"
- "AWX-Collection-tests-tower_api_lookup-host2-{{ test_id }}"
group_name: "AWX-Collection-tests-tower_api_lookup-group1-{{ test_id }}"
- name: Get our collection package
tower_meta:
register: tower_meta
- name: Generate the name of our plugin
set_fact:
plugin_name: "{{ tower_meta.prefix }}.tower_api"
- name: Create all of our users
tower_user:
username: "{{ item }}"
is_superuser: true
password: "{{ test_id }}"
loop: "{{ usernames }}"
register: user_creation_results
- block:
- name: Create our hosts
tower_host:
name: "{{ item }}"
inventory: "Demo Inventory"
loop: "{{ hosts }}"
- name: Test too many params (failure from validation of terms)
set_fact:
junk: "{{ query(plugin_name, 'users', 'teams', query_params={}, ) }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'You must pass exactly one endpoint to query' in result.msg"
- name: Try to load invalid endpoint
set_fact:
junk: "{{ query(plugin_name, '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 without promoting objects
set_fact:
users_list: "{{ lookup(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=False) }}"
- assert:
that:
- users_list['results'] | length() == 1
- users_list['count'] == 1
- users_list['results'][0]['id'] == user_creation_results['results'][0]['id']
- name: Load user of a specific name with promoting objects
set_fact:
user_objects: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=True ) }}"
- assert:
that:
- user_objects | length() == 1
- users_list['results'][0]['id'] == user_objects[0]['id']
- name: Loop over one user with the loop syntax
assert:
that:
- item['id'] == user_creation_results['results'][0]['id']
loop: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] } ) }}"
loop_control:
label: "{{ item.id }}"
- name: Get a page of users as just ids
set_fact:
users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}"
- name: Assert that user list has 2 ids only and that they are strings, not ints
assert:
that:
- users | length() == 2
- user_creation_results['results'][0]['id'] not in users
- user_creation_results['results'][0]['id'] | string in users
- name: Get all users of a system through next attribute
set_fact:
users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true ) }}"
- assert:
that:
- users | length() >= 3
- name: Get all of the users created with a max_objects of 1
set_fact:
users: "{{ lookup(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true, max_objects=1 ) }}"
ignore_errors: true
register: max_user_errors
- assert:
that:
- max_user_errors is failed
- "'List view at users returned 3 objects, which is more than the maximum allowed by max_objects' in max_user_errors.msg"
- name: Get the ID of the first user created and verify that it is correct
assert:
that: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_ids=True)[0] }} == {{ user_creation_results['results'][0]['id'] }}"
- name: Try to get an ID of someone who does not exist
set_fact:
failed_user_id: "{{ query(plugin_name, 'users', query_params={ 'username': 'john jacob jingleheimer schmidt' }, expect_one=True) }}"
register: result
ignore_errors: true
- assert:
that:
- result is failed
- "'Expected one object from endpoint users' in result['msg']"
- name: Lookup too many users
set_fact:
too_many_user_ids: " {{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id }, expect_one=True) }}"
register: results
ignore_errors: true
- assert:
that:
- results is failed
- "'Expected one object from endpoint users, but obtained 3' in results['msg']"
- name: Get the ping page
set_fact:
ping_data: "{{ lookup(plugin_name, 'ping' ) }}"
register: results
- assert:
that:
- results is succeeded
- "'active_node' in ping_data"
- name: "Make sure that expect_objects fails on an API page"
set_fact:
my_var: "{{ lookup(plugin_name, 'settings/ui', expect_objects=True) }}"
ignore_errors: true
register: results
- assert:
that:
- results is failed
- "'Did not obtain a list or detail view at settings/ui, and expect_objects or expect_one is set to True' in results.msg"
# DOCS Example Tests
- name: Load the UI settings
set_fact:
tower_settings: "{{ lookup('awx.awx.tower_api', 'settings/ui') }}"
- assert:
that:
- "'CUSTOM_LOGO' in tower_settings"
- name: Display the usernames of all admin users
debug:
msg: "Admin users: {{ query('awx.awx.tower_api', 'users', query_params={ 'is_superuser': true }) | map(attribute='username') | join(', ') }}"
register: results
- assert:
that:
- "'admin' in results.msg"
- name: debug all organizations in a loop # use query to return a list
debug:
msg: "Organization description={{ item['description'] }} id={{ item['id'] }}"
loop: "{{ query('awx.awx.tower_api', 'organizations') }}"
loop_control:
label: "{{ item['name'] }}"
- name: Make sure user 'john' is an org admin of the default org if the user exists
tower_role:
organization: Default
role: admin
user: "{{ usernames[0] }}"
state: absent
register: tower_role_revoke
when: "query('awx.awx.tower_api', 'users', query_params={ 'username': 'DNE_TESTING' }) | length == 1"
- assert:
that:
- tower_role_revoke is skipped
- name: Create an inventory group with all 'foo' hosts
tower_group:
name: "{{ group_name }}"
inventory: "Demo Inventory"
hosts: >-
{{ query(
'awx.awx.tower_api',
'hosts',
query_params={ 'name__endswith' : test_id, },
) | map(attribute='name') | list }}
register: group_creation
- assert:
that: group_creation is changed
always:
- name: Cleanup group
tower_group:
name: "{{ group_name }}"
inventory: "Demo Inventory"
state: absent
- name: Cleanup hosts
tower_host:
name: "{{ item }}"
inventory: "Demo Inventory"
state: absent
loop: "{{ hosts }}"
- name: Cleanup users
tower_user:
username: "{{ item }}"
state: absent
loop: "{{ usernames }}"

View File

@@ -1,7 +1,15 @@
--- ---
- name: Get our collection package
tower_meta:
register: tower_meta
- name: Generate the name of our plugin
set_fact:
plugin_name: "{{ tower_meta.prefix }}.tower_schedule_rrule"
- name: Test too many params (failure from validation of terms) - name: Test too many params (failure from validation of terms)
debug: debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', 'weekly', start_date='2020-4-16 03:45:07') }}" msg: "{{ query(plugin_name, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true ignore_errors: true
register: result register: result
@@ -12,7 +20,7 @@
- name: Test invalid frequency (failure from validation of term) - name: Test invalid frequency (failure from validation of term)
debug: debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'john', start_date='2020-4-16 03:45:07') }}" msg: "{{ query(plugin_name, 'john', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true ignore_errors: true
register: result register: result
@@ -23,7 +31,7 @@
- name: Test an invalid start date (generic failure case from get_rrule) - name: Test an invalid start date (generic failure case from get_rrule)
debug: debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='invalid') }}" msg: "{{ query(plugin_name, 'none', start_date='invalid') }}"
ignore_errors: true ignore_errors: true
register: result register: result
@@ -34,7 +42,7 @@
- name: Test end_on as count (generic success case) - name: Test end_on as count (generic success case)
debug: debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}" msg: "{{ query(plugin_name, 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}"
register: result register: result
- assert: - assert:

View File

@@ -11,7 +11,7 @@
replace: replace:
path: "{{ collection_path }}/plugins/module_utils/tower_api.py" path: "{{ collection_path }}/plugins/module_utils/tower_api.py"
regexp: '^ _COLLECTION_TYPE = "awx"' regexp: '^ _COLLECTION_TYPE = "awx"'
replace: ' _COLLECTION_TYPE = "{{ collection_namespace }}"' replace: ' _COLLECTION_TYPE = "{{ collection_package }}"'
- name: Do file content replacements for non-default namespace or package name - name: Do file content replacements for non-default namespace or package name
block: block:
@@ -19,9 +19,12 @@
- name: Change module doc_fragments to support desired namespace and package names - name: Change module doc_fragments to support desired namespace and package names
replace: replace:
path: "{{ item }}" path: "{{ item }}"
regexp: '^extends_documentation_fragment: awx.awx.auth' regexp: '^extends_documentation_fragment: awx.awx.auth([a-zA-Z0-9_]*)$'
replace: 'extends_documentation_fragment: {{ collection_namespace }}.{{ collection_package }}.auth' replace: 'extends_documentation_fragment: {{ collection_namespace }}.{{ collection_package }}.auth\1'
with_fileglob: "{{ collection_path }}/plugins/modules/tower_*.py" with_fileglob:
- "{{ collection_path }}/plugins/inventory/*.py"
- "{{ collection_path }}/plugins/lookup/*.py"
- "{{ collection_path }}/plugins/modules/tower_*.py"
loop_control: loop_control:
label: "{{ item | basename }}" label: "{{ item | basename }}"