From b9d2e431a6ad09fe40f2b6ee415c2dcccbff2559 Mon Sep 17 00:00:00 2001 From: Geoffrey Bachelot Date: Thu, 6 Aug 2020 11:52:50 +0200 Subject: [PATCH 1/7] Create tower_application module --- .../plugins/modules/tower_application.py | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 awx_collection/plugins/modules/tower_application.py diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py new file mode 100644 index 0000000000..1ffb53fe2d --- /dev/null +++ b/awx_collection/plugins/modules/tower_application.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020,Geoffrey Bachelot +# 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_application +author: "Geoffrey Bacheot (@jffz)" +short_description: create, update, or destroy Ansible Tower applications +description: + - Create, update, or destroy Ansible Tower applications. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name of the application. + required: True + type: str + description: + description: + - Description of the application. + type: str + authorization_grant_type: + description: + - The grant type the user must use for acquire tokens for this application. + default: "password" + choices: ["password", "authorization-code"] + type: str + required: True + client_id: + description: + - Desired client_id for application. Self generated if empty, returned in response + type: str + # TODO it can be defined but is never returned through api, need to check + client_secret: + description: + - Desired client_secret for application. Self generated, returned in response + type: str + client_type: + description: + - Set to public or confidential depending on how secure the client device is. + choices: ["public", "confidential"] + type: str + required: True + organization: + description: + - Name of organization for application. + type: str + required: True + redirect_uris: + description: + - Allowed urls list, space separated. Required when authorization-grant-type=authorization-code + default: "present" + choices: ["present", "absent"] + type: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + host: + description: + - Host for this credential. + - Deprecated, will be removed in a future release + type: str + username: + description: + - Username for this credential. ``access_key`` for AWS. + - Deprecated, please use inputs + type: str + password: + description: + - Password for this credential. ``secret_key`` for AWS. ``api_key`` for RAX. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str +''' + + +EXAMPLES = ''' +- name: Add Foo application + tower_application: + name: "Foo" + description: "Foo bar application" + organization: "test" + state: present + authorization-grant-type: password + client-type: public + +- name: Add Foo application + tower_application: + name: "Foo" + description: "Foo bar application" + organization: "test" + state: present + authorization-grant-type: authorization-code + client-type: confidential + redirect_uris: http://tower.com/api/v2/ +''' + +import time + +from ..module_utils.tower_api import TowerModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(), + authorization_grant_type=dict(required=True), + client_type=dict(required=True, choices=['public', 'confidential']), + organization=dict(required=True), + redirect_uris=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + host=dict(), + username=dict(), + password=dict(no_log=True) + ) + + # Create a module for ourselves + module = TowerModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + description = module.params.get('description') + authorization_grant_type = module.params.get('authorization_grant_type') + client_type = module.params.get('client_type') + organization = module.params.get('organization') + redirect_uris = ' '.join(module.params.get('redirect_uris')) + state = module.params.get('state') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + org_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up application based on the provided name and org ID + application = module.get_one('applications', **{ + 'data': { + 'name': name, + 'organization': org_id + } + }) + + if state == 'absent': + # 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(application) + + # Attempt to look up associated field items the user specified. + association_fields = {} + + # Create the data that gets sent for create and update + application_fields = { + 'name': name, + 'authorization_grant_type': authorization_grant_type, + 'client_type': client_type, + 'organization': org_id, + } + if description is not None: + application_fields['description'] = description + if redirect_uris is not None: + application_fields['redirect_uris'] = redirect_uris + + if authorization_grant_type == 'authorization-code' and not redirect_uris: + module.fail_json(msg='Parameter redirect_uris is required when authorization-grant-type is authorization code based.') + + # If the state was present and we can let the module build or update the existing application, this will return on its own + module.create_or_update_if_needed( + application, application_fields, + endpoint='applications', item_type='application' + ) + + +if __name__ == '__main__': + main() From 5a374585de8e705aa57b1ab5ca6549c9d40fac8f Mon Sep 17 00:00:00 2001 From: Geoffrey Bachelot Date: Mon, 31 Aug 2020 10:51:59 +0200 Subject: [PATCH 2/7] delete deprecated parameters and add missing skip_authorization --- .../plugins/modules/tower_application.py | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py index 1ffb53fe2d..056d805f22 100644 --- a/awx_collection/plugins/modules/tower_application.py +++ b/awx_collection/plugins/modules/tower_application.py @@ -38,15 +38,6 @@ options: choices: ["password", "authorization-code"] type: str required: True - client_id: - description: - - Desired client_id for application. Self generated if empty, returned in response - type: str - # TODO it can be defined but is never returned through api, need to check - client_secret: - description: - - Desired client_secret for application. Self generated, returned in response - type: str client_type: description: - Set to public or confidential depending on how secure the client device is. @@ -70,22 +61,16 @@ options: default: "present" choices: ["present", "absent"] type: str - host: + skip_authorization: description: - - Host for this credential. - - Deprecated, will be removed in a future release - type: str + - Set True to skip authorization step for completely trusted applications. + default: false + type: bool username: description: - Username for this credential. ``access_key`` for AWS. - Deprecated, please use inputs type: str - password: - description: - - Password for this credential. ``secret_key`` for AWS. ``api_key`` for RAX. - - Use "ASK" and launch in Tower to be prompted. - - Deprecated, please use inputs - type: str ''' @@ -125,9 +110,8 @@ def main(): organization=dict(required=True), redirect_uris=dict(type="list", elements='str'), state=dict(choices=['present', 'absent'], default='present'), - host=dict(), - username=dict(), - password=dict(no_log=True) + skip_authorization=dict(type=bool), + username=dict() ) # Create a module for ourselves From 1c729518a5e96e5a6978baf032c4f1c5a57584c1 Mon Sep 17 00:00:00 2001 From: Geoffrey Bachelot Date: Tue, 1 Sep 2020 14:27:10 +0200 Subject: [PATCH 3/7] Delete depcreated username parameter --- awx_collection/plugins/modules/tower_application.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py index 056d805f22..9efbd1d72d 100644 --- a/awx_collection/plugins/modules/tower_application.py +++ b/awx_collection/plugins/modules/tower_application.py @@ -52,8 +52,6 @@ options: redirect_uris: description: - Allowed urls list, space separated. Required when authorization-grant-type=authorization-code - default: "present" - choices: ["present", "absent"] type: str state: description: @@ -66,11 +64,6 @@ options: - Set True to skip authorization step for completely trusted applications. default: false type: bool - username: - description: - - Username for this credential. ``access_key`` for AWS. - - Deprecated, please use inputs - type: str ''' @@ -110,8 +103,7 @@ def main(): organization=dict(required=True), redirect_uris=dict(type="list", elements='str'), state=dict(choices=['present', 'absent'], default='present'), - skip_authorization=dict(type=bool), - username=dict() + skip_authorization=dict(type=bool) ) # Create a module for ourselves @@ -156,9 +148,6 @@ def main(): if redirect_uris is not None: application_fields['redirect_uris'] = redirect_uris - if authorization_grant_type == 'authorization-code' and not redirect_uris: - module.fail_json(msg='Parameter redirect_uris is required when authorization-grant-type is authorization code based.') - # If the state was present and we can let the module build or update the existing application, this will return on its own module.create_or_update_if_needed( application, application_fields, From c5df37777bf4b0d6562bb457b50a9b96f32eb2f7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 22 Sep 2020 15:22:35 -0400 Subject: [PATCH 4/7] Addig tests and updating minor module bugs --- .../plugins/modules/tower_application.py | 32 ++++---- awx_collection/test/awx/test_application.py | 28 +++++++ .../targets/tower_application/tasks/main.yml | 79 +++++++++++++++++++ 3 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 awx_collection/test/awx/test_application.py create mode 100644 awx_collection/tests/integration/targets/tower_application/tasks/main.yml diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py index 9efbd1d72d..acadc8e66e 100644 --- a/awx_collection/plugins/modules/tower_application.py +++ b/awx_collection/plugins/modules/tower_application.py @@ -37,13 +37,13 @@ options: default: "password" choices: ["password", "authorization-code"] type: str - required: True + required: False client_type: description: - Set to public or confidential depending on how secure the client device is. choices: ["public", "confidential"] type: str - required: True + required: False organization: description: - Name of organization for application. @@ -74,7 +74,7 @@ EXAMPLES = ''' description: "Foo bar application" organization: "test" state: present - authorization-grant-type: password + authorization_grant_type: password client-type: public - name: Add Foo application @@ -83,14 +83,15 @@ EXAMPLES = ''' description: "Foo bar application" organization: "test" state: present - authorization-grant-type: authorization-code + authorization_grant_type: authorization-code client-type: confidential - redirect_uris: http://tower.com/api/v2/ + redirect_uris: + - http://tower.com/api/v2/ ''' import time -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -98,8 +99,8 @@ def main(): argument_spec = dict( name=dict(required=True), description=dict(), - authorization_grant_type=dict(required=True), - client_type=dict(required=True, choices=['public', 'confidential']), + authorization_grant_type=dict(choices=["password", "authorization-code"]), + client_type=dict(choices=['public', 'confidential']), organization=dict(required=True), redirect_uris=dict(type="list", elements='str'), state=dict(choices=['present', 'absent'], default='present'), @@ -107,7 +108,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') @@ -115,16 +116,15 @@ def main(): authorization_grant_type = module.params.get('authorization_grant_type') client_type = module.params.get('client_type') organization = module.params.get('organization') - redirect_uris = ' '.join(module.params.get('redirect_uris')) + redirect_uris = module.params.get('redirect_uris') state = module.params.get('state') # Attempt to look up the related items the user specified (these will fail the module if not found) org_id = module.resolve_name_to_id('organizations', organization) # Attempt to look up application based on the provided name and org ID - application = module.get_one('applications', **{ + application = module.get_one('applications', name_or_id=name, **{ 'data': { - 'name': name, 'organization': org_id } }) @@ -139,14 +139,16 @@ def main(): # Create the data that gets sent for create and update application_fields = { 'name': name, - 'authorization_grant_type': authorization_grant_type, - 'client_type': client_type, 'organization': org_id, } + if authorization_grant_type is not None: + application_fields['authorization_grant_type'] = authorization_grant_type + if client_type is not None: + application_fields['client_type'] = client_type if description is not None: application_fields['description'] = description if redirect_uris is not None: - application_fields['redirect_uris'] = redirect_uris + application_fields['redirect_uris'] = ' '.join(redirect_uris) # If the state was present and we can let the module build or update the existing application, this will return on its own module.create_or_update_if_needed( diff --git a/awx_collection/test/awx/test_application.py b/awx_collection/test/awx/test_application.py new file mode 100644 index 0000000000..1ccb78c7d2 --- /dev/null +++ b/awx_collection/test/awx/test_application.py @@ -0,0 +1,28 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Organization, Application + + +@pytest.mark.django_db +def test_create_application(run_module, admin_user): + org = Organization.objects.create(name='foo') + + module_args = { + 'name': 'foo_app', + 'description': 'barfoo', + 'state': 'present', + 'authorization_grant_type': 'password', + 'client_type': 'public', + 'organization': 'foo', + } + + result = run_module('tower_application', module_args, admin_user) + assert result.get('changed'), result + + application = Application.objects.get(name='foo_app') + assert application.description == 'barfoo' + assert application.organization_id == org.id + diff --git a/awx_collection/tests/integration/targets/tower_application/tasks/main.yml b/awx_collection/tests/integration/targets/tower_application/tasks/main.yml new file mode 100644 index 0000000000..212aec884b --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_application/tasks/main.yml @@ -0,0 +1,79 @@ +--- +- name: Generate a test id + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Generate names + set_fact: + app1_name: "AWX-Collection-tests-tower_application-app1-{{ test_id }}" + app2_name: "AWX-Collection-tests-tower_application-app2-{{ test_id }}" + app3_name: "AWX-Collection-tests-tower_application-app3-{{ test_id }}" + +- block: + - name: Create an application + tower_application: + name: "{{ app1_name }}" + authorization_grant_type: "password" + client_type: "public" + organization: "Default" + state: present + register: result + + - assert: + that: + - "result is changed" + + - name: Delete our application + tower_application: + name: "{{ app1_name }}" + organization: "Default" + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Create a second application + tower_application: + name: "{{ app2_name }}" + authorization_grant_type: "authorization-code" + client_type: "confidential" + organization: "Default" + description: "Another application" + redirect_uris: + - http://tower.com/api/v2/ + - http://tower.com/api/v2/teams + state: present + register: result + + - assert: + that: + - "result is changed" + + - name: Create an all trusting application + tower_application: + name: "{{ app3_name }}" + organization: "Default" + description: "All Trusting Application" + skip_authorization: True + authorization_grant_type: "password" + client_type: "confidential" + state: present + register: result + + - assert: + that: + - "result is changed" + + always: + - name: Delete our application + tower_application: + name: "{{ item }}" + organization: "Default" + state: absent + register: result + loop: + - "{{ app1_name }}" + - "{{ app2_name }}" + - "{{ app3_name }}" From cb1ba9e3a4eafbd26753caedbb8ffd72ec92c444 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 23 Sep 2020 09:03:03 -0400 Subject: [PATCH 5/7] Fixing zuul issues --- awx_collection/test/awx/test_application.py | 5 +++-- .../integration/targets/tower_application/tasks/main.yml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/awx_collection/test/awx/test_application.py b/awx_collection/test/awx/test_application.py index 1ccb78c7d2..a646a30bb7 100644 --- a/awx_collection/test/awx/test_application.py +++ b/awx_collection/test/awx/test_application.py @@ -3,7 +3,8 @@ __metaclass__ = type import pytest -from awx.main.models import Organization, Application +from awx.main.models import Organization +from awx.main.models.oauth import OAuth2AccessToken, OAuth2Application @pytest.mark.django_db @@ -22,7 +23,7 @@ def test_create_application(run_module, admin_user): result = run_module('tower_application', module_args, admin_user) assert result.get('changed'), result - application = Application.objects.get(name='foo_app') + application = OAuth2Application.objects.get(name='foo_app') assert application.description == 'barfoo' assert application.organization_id == org.id diff --git a/awx_collection/tests/integration/targets/tower_application/tasks/main.yml b/awx_collection/tests/integration/targets/tower_application/tasks/main.yml index 212aec884b..cde2440075 100644 --- a/awx_collection/tests/integration/targets/tower_application/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_application/tasks/main.yml @@ -56,7 +56,7 @@ name: "{{ app3_name }}" organization: "Default" description: "All Trusting Application" - skip_authorization: True + skip_authorization: true authorization_grant_type: "password" client_type: "confidential" state: present From 4a4e62e035ff7307b3e868108a5c0ed48d79406d Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 23 Sep 2020 09:54:06 -0400 Subject: [PATCH 6/7] Removing tower_application from needs_development in completeness test --- awx_collection/test/awx/test_completeness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 5dc232e2ed..bf8e4f835a 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -54,7 +54,7 @@ no_api_parameter_ok = { # that needs to be developed. If the module is found on the file system it will auto-detect that the # work is being done and will bypass this check. At some point this module should be removed from this list. needs_development = [ - 'tower_ad_hoc_command', 'tower_application', 'tower_inventory_script', 'tower_workflow_approval' + 'tower_ad_hoc_command', 'tower_inventory_script', 'tower_workflow_approval' ] needs_param_development = { 'tower_host': ['instance_id'], From a9ea2523c9a326945cbb46f6d66cd8a0271ed788 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 24 Sep 2020 13:33:35 -0400 Subject: [PATCH 7/7] Fixing linting/doc issues --- awx_collection/plugins/modules/tower_application.py | 9 +++++---- awx_collection/test/awx/test_application.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/modules/tower_application.py b/awx_collection/plugins/modules/tower_application.py index acadc8e66e..54f53417d2 100644 --- a/awx_collection/plugins/modules/tower_application.py +++ b/awx_collection/plugins/modules/tower_application.py @@ -34,7 +34,6 @@ options: authorization_grant_type: description: - The grant type the user must use for acquire tokens for this application. - default: "password" choices: ["password", "authorization-code"] type: str required: False @@ -52,7 +51,8 @@ options: redirect_uris: description: - Allowed urls list, space separated. Required when authorization-grant-type=authorization-code - type: str + type: list + elements: str state: description: - Desired state of the resource. @@ -62,8 +62,9 @@ options: skip_authorization: description: - Set True to skip authorization step for completely trusted applications. - default: false type: bool + +extends_documentation_fragment: awx.awx.auth ''' @@ -104,7 +105,7 @@ def main(): organization=dict(required=True), redirect_uris=dict(type="list", elements='str'), state=dict(choices=['present', 'absent'], default='present'), - skip_authorization=dict(type=bool) + skip_authorization=dict(type='bool') ) # Create a module for ourselves diff --git a/awx_collection/test/awx/test_application.py b/awx_collection/test/awx/test_application.py index a646a30bb7..ad5f99d430 100644 --- a/awx_collection/test/awx/test_application.py +++ b/awx_collection/test/awx/test_application.py @@ -26,4 +26,3 @@ def test_create_application(run_module, admin_user): application = OAuth2Application.objects.get(name='foo_app') assert application.description == 'barfoo' assert application.organization_id == org.id -