From e6416d770b75c37988f56bcef9d0f37d4c59b3a4 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 21 May 2020 13:14:10 -0400 Subject: [PATCH] Initial cut at tower_token module --- .../plugins/module_utils/tower_api.py | 10 ++ awx_collection/plugins/modules/tower_token.py | 157 ++++++++++++++++++ awx_collection/test/awx/test_token.py | 28 ++++ .../targets/tower_token/tasks/main.yml | 25 +++ 4 files changed, 220 insertions(+) create mode 100644 awx_collection/plugins/modules/tower_token.py create mode 100644 awx_collection/test/awx/test_token.py create mode 100644 awx_collection/tests/integration/targets/tower_token/tasks/main.yml diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 5d4f221f4e..f950829328 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -96,6 +96,13 @@ class TowerModule(AnsibleModule): if direct_value is not None: setattr(self, short_param, direct_value) + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + if type(self.params.get('tower_oauthtoken')) is dict: + setattr(self, 'oauth_token', self.params.get('tower_oauthtoken')['token']) + elif 'token' in self.params.get('tower_oauthtoken'): + setattr(self, 'oauth_token', self.params.get('tower_oauthtoken')) + # Perform some basic validation if not re.match('^https{0,1}://', self.host): self.host = "https://{0}".format(self.host) @@ -504,6 +511,9 @@ class TowerModule(AnsibleModule): item_name = existing_item['username'] elif 'identifier' in existing_item: item_name = existing_item['identifier'] + elif item_type == 'o_auth2_access_token': + # An oauth2 token has no name, instead we will use its id for any of the messages + item_name = existing_item['id'] else: self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type)) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py new file mode 100644 index 0000000000..7e229ac2c1 --- /dev/null +++ b/awx_collection/plugins/modules/tower_token.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV +# 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_token +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower tokens. +description: + - Create, update, or destroy Ansible Tower tokens. See + U(https://www.ansible.com/tower) for an overview. +options: + description: + description: + - Optional description of this access token. + required: False + type: str + default: '' + application: + description: + - The application tied to this token. + required: False + type: str + scope: + description: + - Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']. + required: False + type: str + default: 'write' + choices: ["read", "write"] + existing_token: + description: An existing token (for use with state absent) + type: dict + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str + version_added: "3.7" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Create a new token using an existing token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_oauthtoken: "{{ ny_existing_token }}" + register: new_token + +- name: Use our new token to make another call + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + +- name: Delete our Token with the token we created + tower_token: + existing_token: "{{ tower_token }}" + state: absent +''' + +RETURNS = ''' +tower_token: + type: dict + description: A Tower token object which can be used for auth or token deletion + returned: on successful create +''' + +from ..module_utils.tower_api import TowerModule + + +def return_token(module, last_response): + # A token is special because you can never get the actual token ID back from the API. + # So the default module return would give you an ID but then the token would forever be masked on you. + # This method will return the entire token object we got back so that a user has access to the token + + module.json_output['token'] = last_response['token'] + module.json_output['ansible_facts'] = { + 'tower_token': last_response, + } + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + description=dict(), + application=dict(), + scope=dict(choices=['read', 'write'], default='write'), + existing_token=dict(type='dict'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerModule(argument_spec=argument_spec) + + # Extract our parameters + description = module.params.get('description') + application = module.params.get('application') + scope = module.params.get('scope') + existing_token = module.params.get('existing_token') + state = module.params.get('state') + + with open('/tmp/john', 'w') as f: + f.write("State is {0}".format(state)) + + if state == 'absent': + with open('/tmp/john', 'a') as f: + f.write("Starting delete") + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_token) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + application_id = None + if application: + application_id = module.resolve_name_to_id('applications', application) + + # Create the data that gets sent for create and update + new_fields = {} + if description is not None: + new_fields['description'] = description + if application is not None: + new_fields['application'] = application_id + if scope is not None: + new_fields['scope'] = scope + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + None, new_fields, + endpoint='tokens', item_type='token', + associations={ + }, + on_create=return_token, + ) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/test/awx/test_token.py b/awx_collection/test/awx/test_token.py new file mode 100644 index 0000000000..107a833af6 --- /dev/null +++ b/awx_collection/test/awx/test_token.py @@ -0,0 +1,28 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import OAuth2AccessToken + + +@pytest.mark.django_db +def test_create_token(run_module, admin_user): + + module_args = { + 'description': 'barfoo', + 'state': 'present', + 'scope': 'read', + 'tower_host': None, + 'tower_username': None, + 'tower_password': None, + 'validate_certs': None, + 'tower_oauthtoken': None, + 'tower_config_file': None, + } + + result = run_module('tower_token', module_args, admin_user) + assert result, result.get('changed') + + tokens = OAuth2AccessToken.objects.filter(description='barfoo') + assert len(tokens) == 1, tokens[0].description == 'barfoo' diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml new file mode 100644 index 0000000000..74fcd05237 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Generate names + set_fact: + token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create a Token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: new_token + +- name: Validate our token works by token + tower_job_list: + tower_oauthtoken: "{{ tower_token.token }}" + +- name: Validate out token works by object + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + +- name: Delete our Token with our own token + tower_token: + existing_token: "{{ tower_token }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent