From e6416d770b75c37988f56bcef9d0f37d4c59b3a4 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 21 May 2020 13:14:10 -0400 Subject: [PATCH 01/14] 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 From e50c8fc9c9b4c37a0af17b525bc910a39f195366 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 21 May 2020 14:10:14 -0400 Subject: [PATCH 02/14] Updated docs and added block on tests --- awx_collection/plugins/modules/tower_token.py | 33 +++++++++-------- .../targets/tower_token/tasks/main.yml | 37 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 7e229ac2c1..638b89cc6c 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -20,8 +20,9 @@ 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 + - Create or destroy Ansible Tower tokens. See U(https://www.ansible.com/tower) for an overview. + - If you create a token it is your responsibility to delete the token. options: description: description: @@ -60,22 +61,24 @@ 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 +- block: + - name: Create a new token using an existing token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_oauthtoken: "{{ ny_existing_token }}" -- name: Use our new token to make another call - tower_job_list: - tower_oauthtoken: "{{ tower_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 + always: + - name: Delete our Token with the token we created + tower_token: + existing_token: "{{ tower_token }}" + state: absent + when: tower_token is defined ''' RETURNS = ''' diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 74fcd05237..86edd620d7 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -3,23 +3,26 @@ 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 +- block: + - 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 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: 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 + always: + - name: Delete our Token with our own token + tower_token: + existing_token: "{{ tower_token }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined From 6e9a43513ea9d5116ea6a3713231eca76e323e87 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 21 May 2020 14:14:14 -0400 Subject: [PATCH 03/14] Removed some debugging and added an exmaple --- awx_collection/plugins/modules/tower_token.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 638b89cc6c..8689f1625d 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -69,6 +69,19 @@ EXAMPLES = ''' state: present tower_oauthtoken: "{{ ny_existing_token }}" + - name: Delete this token + tower_token: + existing_token: "{{ tower_token }}" + state: absent + + - name: Create a new token using username/password + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_username: "{{ ny_username }}" + tower_password: "{{ ny_password }}" + - name: Use our new token to make another call tower_job_list: tower_oauthtoken: "{{ tower_token }}" @@ -123,12 +136,7 @@ def main(): 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) From 1dbea4614ba4d29ab7535649fe5cef9704451fa0 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 26 May 2020 12:30:43 -0400 Subject: [PATCH 04/14] Rebasing and fixing oauthtoken docs --- awx_collection/plugins/doc_fragments/auth.py | 3 ++- awx_collection/plugins/modules/tower_token.py | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py index 21e8d07482..c4cc6fcc3f 100644 --- a/awx_collection/plugins/doc_fragments/auth.py +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -32,7 +32,8 @@ options: description: - The Tower OAuth token to use. - If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files - type: str + type: raw + version_added: "3.7" validate_certs: description: - Whether to allow insecure connections to Tower or AWX. diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 8689f1625d..58990b783d 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -51,12 +51,6 @@ options: 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 ''' From 1a0407ba50c834800870eec1699566316e9f767a Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 1 Jun 2020 13:32:00 -0400 Subject: [PATCH 05/14] Fixing ny -> my typos --- awx_collection/plugins/modules/tower_token.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 58990b783d..68d5be87ac 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -61,7 +61,7 @@ EXAMPLES = ''' description: '{{ token_description }}' scope: "write" state: present - tower_oauthtoken: "{{ ny_existing_token }}" + tower_oauthtoken: "{{ my_existing_token }}" - name: Delete this token tower_token: @@ -73,8 +73,8 @@ EXAMPLES = ''' description: '{{ token_description }}' scope: "write" state: present - tower_username: "{{ ny_username }}" - tower_password: "{{ ny_password }}" + tower_username: "{{ my_username }}" + tower_password: "{{ my_password }}" - name: Use our new token to make another call tower_job_list: From 7c88a51992af7fdcc96a2370906c23ac755d6008 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 1 Jun 2020 13:32:56 -0400 Subject: [PATCH 06/14] Adding more details about tower_oauthtoken parameter --- awx_collection/plugins/doc_fragments/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py index c4cc6fcc3f..1e77a63b4b 100644 --- a/awx_collection/plugins/doc_fragments/auth.py +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -31,6 +31,9 @@ options: tower_oauthtoken: description: - The Tower OAuth token to use. + - This value can be in one of two formats. + - A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX) + - A dictionary structure as returned by the tower_token module. - If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files type: raw version_added: "3.7" From 30346618f161ed9eefc1563d25b6924017f26c29 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 1 Jun 2020 13:35:28 -0400 Subject: [PATCH 07/14] Adding more through testing of tower_oauthtoken including failure messages --- .../plugins/module_utils/tower_api.py | 16 ++++++++---- .../targets/tower_token/tasks/main.yml | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index f950829328..7c2f52d1ca 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -3,7 +3,7 @@ __metaclass__ = type from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2 +from ansible.module_utils.six import PY2, string_types from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError @@ -98,10 +98,16 @@ class TowerModule(AnsibleModule): # 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')) + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + self.fail_json(msg="The provided tower_oauthtoken type was not valid ({0}), please refer to ansible-doc for valid options".format(type(token_param).__name__)) # Perform some basic validation if not re.match('^https{0,1}://', self.host): diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 86edd620d7..52e5bb41d8 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -3,6 +3,30 @@ set_fact: token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" +- name: Try to use a token as a dict which is missing the token parameter + tower_job_list: + tower_oauthtoken: + not_token: "This has no token entry" + register: results + ignore_errors: True + +- assert: + that: + - results is failed + - '"The provided dict in tower_oauthtoken did not properly contain the token entry" == results.msg' + +- name: Try to use a token as a list + tower_job_list: + tower_oauthtoken: + - dummy_token + register: results + ignore_errors: True + +- assert: + that: + - results is failed + - '"The provided tower_oauthtoken type was not valid (list), please refer to ansible-doc for valid options" == results.msg' + - block: - name: Create a Token tower_token: @@ -14,10 +38,12 @@ - name: Validate our token works by token tower_job_list: tower_oauthtoken: "{{ tower_token.token }}" + register: job_list - name: Validate out token works by object tower_job_list: tower_oauthtoken: "{{ tower_token }}" + register: job_list always: - name: Delete our Token with our own token From 11da8e254d45ae65b6a71fb30192969ad121ff94 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 2 Jun 2020 10:18:20 -0400 Subject: [PATCH 08/14] Changing documention and fixing sanity tests --- awx_collection/plugins/module_utils/tower_api.py | 3 ++- awx_collection/plugins/modules/tower_token.py | 14 ++++++++++---- .../integration/targets/tower_token/tasks/main.yml | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 7c2f52d1ca..ca5a365e8e 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -107,7 +107,8 @@ class TowerModule(AnsibleModule): elif isinstance(token_param, string_types): self.oauth_token = self.params.get('tower_oauthtoken') else: - self.fail_json(msg="The provided tower_oauthtoken type was not valid ({0}), please refer to ansible-doc for valid options".format(type(token_param).__name__)) + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) # Perform some basic validation if not re.match('^https{0,1}://', self.host): diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 68d5be87ac..d620e7a18a 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -22,7 +22,10 @@ short_description: create, update, or destroy Ansible Tower tokens. description: - Create or destroy Ansible Tower tokens. See U(https://www.ansible.com/tower) for an overview. - - If you create a token it is your responsibility to delete the token. + - In addition, the module sets an Ansible fact which can be passed into other + tower_* modules as the parameter tower_oauthtoken. See examples for usage. + - Because of the sensitive nature of tokens, the created token value is only available once + through the Ansible fact. (See RETURN for details) options: description: description: @@ -88,10 +91,14 @@ EXAMPLES = ''' when: tower_token is defined ''' -RETURNS = ''' +RETURN = ''' tower_token: type: dict - description: A Tower token object which can be used for auth or token deletion + description: An Ansible Fact variable representing a Tower token object which can be used for auth in subsequent modules. See examples for usage. + contains: + token: + description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost. + type: str returned: on successful create ''' @@ -103,7 +110,6 @@ def return_token(module, last_response): # 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, } diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 52e5bb41d8..8874e21e22 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -25,7 +25,7 @@ - assert: that: - results is failed - - '"The provided tower_oauthtoken type was not valid (list), please refer to ansible-doc for valid options" == results.msg' + - '"The provided tower_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg' - block: - name: Create a Token From 41d0548af67fce58c67037f32c084914e7c15aa7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 2 Jun 2020 13:07:11 -0400 Subject: [PATCH 09/14] Fixing truthy linting issue --- .../tests/integration/targets/tower_token/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 8874e21e22..0ba32c19b6 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -8,7 +8,7 @@ tower_oauthtoken: not_token: "This has no token entry" register: results - ignore_errors: True + ignore_errors: true - assert: that: @@ -20,7 +20,7 @@ tower_oauthtoken: - dummy_token register: results - ignore_errors: True + ignore_errors: true - assert: that: From 0e54f76f801a43e5a4e8615b315e876cb7ac94fc Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 5 Jun 2020 16:10:26 -0400 Subject: [PATCH 10/14] Fixing missing raw from merge type --- awx_collection/plugins/module_utils/tower_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index ca5a365e8e..d836c457c0 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -47,7 +47,7 @@ class TowerModule(AnsibleModule): tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), tower_config_file=dict(type='path', required=False, default=None), ) short_params = { From 1f17e02fe9988fe3139d1f6dd4d876c61ceebe76 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 5 Jun 2020 16:11:26 -0400 Subject: [PATCH 11/14] Adding more documenation and added existing_token_id param --- awx_collection/plugins/modules/tower_token.py | 28 ++++++++++++- .../targets/tower_token/tasks/main.yml | 41 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index d620e7a18a..3ee2e2b504 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -26,6 +26,10 @@ description: tower_* modules as the parameter tower_oauthtoken. See examples for usage. - Because of the sensitive nature of tokens, the created token value is only available once through the Ansible fact. (See RETURN for details) + - Due to the nature of tokens in Tower this module is not idempotent. A second will + with the same parameters will create a new token. + - If you are creating a temporary token for use with modules you should delete the token + when you are done with it. See the example for how to do it. options: description: description: @@ -46,8 +50,11 @@ options: default: 'write' choices: ["read", "write"] existing_token: - description: An existing token (for use with state absent) + description: The data structure produced from tower_token in create mode to be used with state absent. type: dict + existing_token_id: + description: A token ID (number) which can be used to delete an arbitrary token with state absent. + type: str state: description: - Desired state of the resource. @@ -89,6 +96,11 @@ EXAMPLES = ''' existing_token: "{{ tower_token }}" state: absent when: tower_token is defined + +- name: Delete a token by its id + tower_toekn: + existing_token_id: 4 + state: absent ''' RETURN = ''' @@ -99,6 +111,9 @@ tower_token: token: description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost. type: str + id: + description: The numeric ID of the token created + type: str returned: on successful create ''' @@ -123,6 +138,7 @@ def main(): application=dict(), scope=dict(choices=['read', 'write'], default='write'), existing_token=dict(type='dict'), + existing_token_id=dict(), state=dict(choices=['present', 'absent'], default='present'), ) @@ -134,9 +150,19 @@ def main(): application = module.params.get('application') scope = module.params.get('scope') existing_token = module.params.get('existing_token') + existing_token_id = module.params.get('existing_token_id') state = module.params.get('state') if state == 'absent': + if not existing_token: + if not existing_token_id: + module.fail_json(msg='When deleting a token you specify either the parameter existing_token or existing_token_id') + existing_token = module.get_one('tokens', **{ + 'data': { + 'id': existing_token_id, + } + }) + # 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) diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 0ba32c19b6..0c4d1031c1 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -27,6 +27,17 @@ - results is failed - '"The provided tower_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg' +- name: Try to delete a token with no existing_token or existing_token_id + tower_token: + state: absent + register: results + ignore_errors: True + +- assert: + that: + - results is failed + - '"When deleting a token you specify either the parameter existing_token or existing_token_id" == results.msg' + - block: - name: Create a Token tower_token: @@ -52,3 +63,33 @@ tower_oauthtoken: "{{ tower_token }}" state: absent when: tower_token is defined + register: results + + - assert: + that: + - results is changed or results is skipped + +- block: + - name: Create a second token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: results + + - assert: + that: + - results is changed + + always: + - name: Delete the second Token with our own token + tower_token: + existing_token_id: "{{ tower_token['id'] }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined + register: results + + - assert: + that: + - results is changed or resuslts is skipped From 3b60529488be0725407197af2a3b364d182ae0c0 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 8 Jun 2020 06:42:33 -0400 Subject: [PATCH 12/14] Fixes in testing and parameter processing --- awx_collection/plugins/modules/tower_token.py | 13 ++++++++++--- awx_collection/test/awx/test_token.py | 5 +++-- .../targets/tower_token/tasks/main.yml | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 3ee2e2b504..3be5cebde5 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -143,7 +143,16 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ('existing_token', 'existing_token_id'), + ], + # If we are state absent make sure one of existing_token or existing_token_id are present + required_if=[ + [ 'state', 'absent', ('existing_token', 'existing_token_id'), True,], + ], + ) # Extract our parameters description = module.params.get('description') @@ -155,8 +164,6 @@ def main(): if state == 'absent': if not existing_token: - if not existing_token_id: - module.fail_json(msg='When deleting a token you specify either the parameter existing_token or existing_token_id') existing_token = module.get_one('tokens', **{ 'data': { 'id': existing_token_id, diff --git a/awx_collection/test/awx/test_token.py b/awx_collection/test/awx/test_token.py index 107a833af6..e3f1d2a8c1 100644 --- a/awx_collection/test/awx/test_token.py +++ b/awx_collection/test/awx/test_token.py @@ -22,7 +22,8 @@ def test_create_token(run_module, admin_user): } result = run_module('tower_token', module_args, admin_user) - assert result, result.get('changed') + assert result.get('changed'), result tokens = OAuth2AccessToken.objects.filter(description='barfoo') - assert len(tokens) == 1, tokens[0].description == 'barfoo' + assert len(tokens) == 1, 'Rokens with description of barfoo != 0: {0}'.format(len(tokens)) + assert tokens[0].scope == 'read', 'Token was not given read access' diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 0c4d1031c1..2854d4e6ca 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -36,7 +36,22 @@ - assert: that: - results is failed - - '"When deleting a token you specify either the parameter existing_token or existing_token_id" == results.msg' + # We don't assert a message here because it handled by ansible + +- name: Try to delete a token with both existing_token or existing_token_id + tower_token: + existing_token: + id: 1234 + existing_token_id: 1234 + state: absent + register: results + ignore_errors: True + +- assert: + that: + - results is failed + # We don't assert a message here because it handled by ansible + - block: - name: Create a Token From 64846c3347fb3a6a7ef76ac15c1030b53fad3d2e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 8 Jun 2020 07:09:45 -0400 Subject: [PATCH 13/14] Fixing truthy issues --- .../tests/integration/targets/tower_token/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml index 2854d4e6ca..355d5dd02f 100644 --- a/awx_collection/tests/integration/targets/tower_token/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_token/tasks/main.yml @@ -31,7 +31,7 @@ tower_token: state: absent register: results - ignore_errors: True + ignore_errors: true - assert: that: @@ -45,7 +45,7 @@ existing_token_id: 1234 state: absent register: results - ignore_errors: True + ignore_errors: true - assert: that: From 76dcd6d72a351468e55896604be60611948c7967 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 8 Jun 2020 07:49:58 -0400 Subject: [PATCH 14/14] Fixing scooby doo fingers --- awx_collection/plugins/modules/tower_token.py | 2 +- awx_collection/test/awx/test_token.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 3be5cebde5..d8300a048e 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -98,7 +98,7 @@ EXAMPLES = ''' when: tower_token is defined - name: Delete a token by its id - tower_toekn: + tower_token: existing_token_id: 4 state: absent ''' diff --git a/awx_collection/test/awx/test_token.py b/awx_collection/test/awx/test_token.py index e3f1d2a8c1..442fa2e9fb 100644 --- a/awx_collection/test/awx/test_token.py +++ b/awx_collection/test/awx/test_token.py @@ -25,5 +25,5 @@ def test_create_token(run_module, admin_user): assert result.get('changed'), result tokens = OAuth2AccessToken.objects.filter(description='barfoo') - assert len(tokens) == 1, 'Rokens with description of barfoo != 0: {0}'.format(len(tokens)) + assert len(tokens) == 1, 'Tokens with description of barfoo != 0: {0}'.format(len(tokens)) assert tokens[0].scope == 'read', 'Token was not given read access'