From 9e29dd08fb0fe3331afb6cc2f5164afa8d039c4b Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 28 Aug 2020 15:25:32 -0400 Subject: [PATCH 1/9] Adding tower_instance_group module --- .../plugins/module_utils/tower_api.py | 2 + .../plugins/modules/tower_instance_group.py | 157 ++++++++++++++++++ .../test/awx/test_instance_group.py | 69 ++++++++ .../tower_instance_group/tasks/main.yml | 63 +++++++ 4 files changed, 291 insertions(+) create mode 100644 awx_collection/plugins/modules/tower_instance_group.py create mode 100644 awx_collection/test/awx/test_instance_group.py create mode 100644 awx_collection/tests/integration/targets/tower_instance_group/tasks/main.yml diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index c42e6733a5..28edd82751 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -109,6 +109,8 @@ class TowerAPIModule(TowerModule): name_field = 'name' if endpoint == 'users': name_field = 'username' + elif endpoint == 'instances': + name_field = 'hostname' query_params = {'or__{0}'.format(name_field): name_or_id} try: diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py new file mode 100644 index 0000000000..c878090ff4 --- /dev/null +++ b/awx_collection/plugins/modules/tower_instance_group.py @@ -0,0 +1,157 @@ +#!/usr/bin/env 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_instance_group +author: "John Westcott IV (@john-westcott-iv)" +version_added: "4.0" +short_description: create, update, or destroy Ansible Tower instance groups. +description: + - Create, update, or destroy Ansible Tower instance groups. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name of this instance group. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name (looked up via the name field. + required: True + type: str + credential: + description: + - Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token”. + required: False + type: str + policy_instance_percentage: + description: + - Minimum percentage of all instances that will be automatically assigned to this group when new instances come online. + required: False + type: int + default: '0' + policy_instance_minimum: + description: + - Static minimum number of Instances that will be automatically assign to this group when new instances come online. + required: False + type: int + default: '0' + policy_instance_list: + description: + - List of exact-match Instances that will be assigned to this group + required: False + type: dict + pod_spec_override: + description: + - A custom Kubernetes or OpenShift Pod specification. + required: False + type: str + instances: + description: + - The instances associated with this instance_group + required: False + type: list + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + credential=dict(), + policy_instance_percentage=dict(type='int', default='0'), + policy_instance_minimum=dict(type='int', default='0'), + policy_instance_list=dict(type='dict'), + pod_spec_override=dict(), + instances=dict(required=False, type="list", default=None), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get("new_name") + credential = module.params.get('credential') + policy_instance_percentage = module.params.get('policy_instance_percentage') + policy_instance_minimum = module.params.get('policy_instance_minimum') + policy_instance_list = module.params.get('policy_instance_list') + pod_spec_override = module.params.get('pod_spec_override') + instances = module.params.get('instances') + state = module.params.get('state') + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('instance_groups', **{ + 'data': { + 'name': name, + } + }) + + if state is '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(existing_item) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + credential_id = None + if credential: + credential_id = module.resolve_name_to_id('credentials', credential) + instances_ids = None + if instances is not None: + instances_ids = [] + for item in instances: + instances_ids.append( module.resolve_name_to_id('instances', item) ) + + # Create the data that gets sent for create and update + new_fields = {} + new_fields['name'] = new_name if new_name else name + if credential is not None: + new_fields['credential'] = credential_id + if policy_instance_percentage is not None: + new_fields['policy_instance_percentage'] = policy_instance_percentage + if policy_instance_minimum is not None: + new_fields['policy_instance_minimum'] = policy_instance_minimum + if policy_instance_list is not None: + new_fields['policy_instance_list'] = policy_instance_list + if pod_spec_override is not None: + new_fields['pod_spec_override'] = pod_spec_override + + # 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( + existing_item, new_fields, + endpoint='instance_groups', item_type='instance_group', + associations={ + 'instances': instances_ids, + } + ) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py new file mode 100644 index 0000000000..cf00299f3c --- /dev/null +++ b/awx_collection/test/awx/test_instance_group.py @@ -0,0 +1,69 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import InstanceGroup, Instance +from awx.main.tests.functional.conftest import kube_credential, credentialtype_kube + +@pytest.mark.django_db +def test_instance_group_create(run_module, admin_user): + result = run_module('tower_instance_group', { + 'name': 'foo-group', + 'policy_instance_percentage': 34, + 'policy_instance_minimum': 12, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result + assert result['changed'] + + ig = InstanceGroup.objects.get(name='foo-group') + assert ig.policy_instance_percentage == 34 + assert ig.policy_instance_minimum == 12 + + # Create a new instance in the DB + new_instance = Instance.objects.create(hostname='foo.example.com') + + # Set the new instance group only to the one instnace + result = run_module('tower_instance_group', { + 'name': 'foo-group', + 'instances': new_instance.hostname, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result + assert result['changed'] + + ig = InstanceGroup.objects.get(name='foo-group') + all_instance_names = [] + for instance in ig.instances.all(): + all_instance_names.append(instance.hostname) + + assert new_instance.hostname in all_instance_names, 'Failed to add instance to group' + assert len(all_instance_names) == 1, 'Too many instances in group {0}'.format(','.join(all_instance_names)) + +@pytest.mark.django_db +def test_continer_group_create(run_module, admin_user, kube_credential): + pod_spec = "{ 'Nothing': True }" + + result = run_module('tower_instance_group', { + 'name': 'foo-c-group', + 'credential': kube_credential.id, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result['msg'] + assert result['changed'] + + ig = InstanceGroup.objects.get(name='foo-c-group') + assert ig.pod_spec_override == '' + + result = run_module('tower_instance_group', { + 'name': 'foo-c-group', + 'credential': kube_credential.id, + 'pod_spec_override': pod_spec, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result['msg'] + assert result['changed'] + + ig = InstanceGroup.objects.get(name='foo-c-group') + assert ig.pod_spec_override == pod_spec diff --git a/awx_collection/tests/integration/targets/tower_instance_group/tasks/main.yml b/awx_collection/tests/integration/targets/tower_instance_group/tasks/main.yml new file mode 100644 index 0000000000..4ae9cfaffc --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_instance_group/tasks/main.yml @@ -0,0 +1,63 @@ +--- +- name: Generate test id + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Generate names + set_fact: + group_name1: "AWX-Collection-tests-tower_instance_group-group1-{{ test_id }}" + group_name2: "AWX-Collection-tests-tower_instance_group-group2-{{ test_id }}" + cred_name1: "AWX-Collection-tests-tower_instance_group-cred1-{{ test_id }}" + +- block: + - name: Create an OpenShift Credential + tower_credential: + name: "{{ cred_name1 }}" + organization: "Default" + credential_type: "OpenShift or Kubernetes API Bearer Token" + inputs: + host: "https://openshift.org" + bearer_token: "asdf1234" + verify_ssl: false + register: result + + - assert: + that: + - "result is changed" + + - name: Create an Instance Group + tower_instance_group: + name: "{{ group_name1 }}" + policy_instance_percentage: 34 + policy_instance_minimum: 12 + state: present + register: result + + - assert: + that: + - "result is changed" + + - name: Create a container group + tower_instance_group: + name: "{{ group_name2 }}" + credential: "{{ cred_name1 }}" + register: result + + - assert: + that: + - "result is changed" + + always: + - name: Delete the instance groups + tower_instance_group: + name: "{{ item }}" + state: absent + loop: + - "{{ group_name1 }}" + - "{{ group_name2 }}" + + - name: Delete the credential + tower_credential: + name: "{{ cred_name1 }}" + organization: "Default" + credential_type: "OpenShift or Kubernetes API Bearer Token" From 2369bcb25c932c87a83897b41587ab6291769009 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 28 Aug 2020 15:27:54 -0400 Subject: [PATCH 2/9] Removing forward feature needed for testing locally --- 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 28edd82751..76a79e414e 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -553,7 +553,7 @@ class TowerAPIModule(TowerModule): return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) def logout(self): - if self.authenticated and self.oauth_token_id: + if self.authenticated: # Attempt to delete our current token from /api/v2/tokens/ # Post to the tokens endpoint with baisc auth to try and get a token api_token_url = ( From 1bd8f4ad3ec940052b0f9cf284f47bd8fc888027 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 28 Aug 2020 15:29:10 -0400 Subject: [PATCH 3/9] Removing tower_instance_group from completness --- awx_collection/test/awx/test_completeness.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 92a743daf4..6154a8c71b 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -53,8 +53,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_instance_group', 'tower_inventory_script', - 'tower_workflow_approval' + 'tower_ad_hoc_command', 'tower_application', 'tower_inventory_script', 'tower_workflow_approval' ] needs_param_development = { 'tower_host': ['instance_id'], From 5a8bcd357b5fc09c845e2a659ccf4d1c2ef63904 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 31 Aug 2020 10:41:25 -0400 Subject: [PATCH 4/9] Fixing linting issues --- awx_collection/plugins/modules/tower_instance_group.py | 6 +++--- awx_collection/test/awx/test_instance_group.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py index c878090ff4..6424be4b3b 100644 --- a/awx_collection/plugins/modules/tower_instance_group.py +++ b/awx_collection/plugins/modules/tower_instance_group.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python # coding: utf-8 -*- @@ -35,7 +35,7 @@ options: type: str credential: description: - - Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token”. + - Credential to authenticate with Kubernetes or OpenShift. Must be of type "Kubernetes/OpenShift API Bearer Token". required: False type: str policy_instance_percentage: @@ -127,7 +127,7 @@ def main(): if instances is not None: instances_ids = [] for item in instances: - instances_ids.append( module.resolve_name_to_id('instances', item) ) + instances_ids.append(module.resolve_name_to_id('instances', item)) # Create the data that gets sent for create and update new_fields = {} diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py index cf00299f3c..b548c59b6e 100644 --- a/awx_collection/test/awx/test_instance_group.py +++ b/awx_collection/test/awx/test_instance_group.py @@ -6,6 +6,7 @@ import pytest from awx.main.models import InstanceGroup, Instance from awx.main.tests.functional.conftest import kube_credential, credentialtype_kube + @pytest.mark.django_db def test_instance_group_create(run_module, admin_user): result = run_module('tower_instance_group', { @@ -41,6 +42,7 @@ def test_instance_group_create(run_module, admin_user): assert new_instance.hostname in all_instance_names, 'Failed to add instance to group' assert len(all_instance_names) == 1, 'Too many instances in group {0}'.format(','.join(all_instance_names)) + @pytest.mark.django_db def test_continer_group_create(run_module, admin_user, kube_credential): pod_spec = "{ 'Nothing': True }" From 574c3b65b272f1995462d3b9b3a25c10c96fb667 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 31 Aug 2020 10:52:58 -0400 Subject: [PATCH 5/9] Converting from string to array --- awx_collection/test/awx/test_instance_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py index b548c59b6e..a5e00d5ab8 100644 --- a/awx_collection/test/awx/test_instance_group.py +++ b/awx_collection/test/awx/test_instance_group.py @@ -28,7 +28,7 @@ def test_instance_group_create(run_module, admin_user): # Set the new instance group only to the one instnace result = run_module('tower_instance_group', { 'name': 'foo-group', - 'instances': new_instance.hostname, + 'instances': [ new_instance.hostname ], 'state': 'present' }, admin_user) assert not result.get('failed', False), result From 72bdd17518fb9d6ff5db87f6f60e25ea9e595468 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 31 Aug 2020 11:00:38 -0400 Subject: [PATCH 6/9] Fixing regression --- 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 76a79e414e..28edd82751 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -553,7 +553,7 @@ class TowerAPIModule(TowerModule): return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) def logout(self): - if self.authenticated: + if self.authenticated and self.oauth_token_id: # Attempt to delete our current token from /api/v2/tokens/ # Post to the tokens endpoint with baisc auth to try and get a token api_token_url = ( From 36ab0dd03e9d18d5fa254a410355144d46bc7fc7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 1 Sep 2020 09:24:36 -0400 Subject: [PATCH 7/9] Fixing white spaces --- awx_collection/test/awx/test_instance_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py index a5e00d5ab8..dc564d8fff 100644 --- a/awx_collection/test/awx/test_instance_group.py +++ b/awx_collection/test/awx/test_instance_group.py @@ -28,7 +28,7 @@ def test_instance_group_create(run_module, admin_user): # Set the new instance group only to the one instnace result = run_module('tower_instance_group', { 'name': 'foo-group', - 'instances': [ new_instance.hostname ], + 'instances': [new_instance.hostname], 'state': 'present' }, admin_user) assert not result.get('failed', False), result From d01f2d6cafdffd6cc5a20bef77128fa06ec47f26 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 1 Sep 2020 11:02:39 -0400 Subject: [PATCH 8/9] Converting policy_instance_list from dict to list --- awx_collection/plugins/modules/tower_instance_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_instance_group.py b/awx_collection/plugins/modules/tower_instance_group.py index 6424be4b3b..9a7db2f814 100644 --- a/awx_collection/plugins/modules/tower_instance_group.py +++ b/awx_collection/plugins/modules/tower_instance_group.py @@ -54,7 +54,7 @@ options: description: - List of exact-match Instances that will be assigned to this group required: False - type: dict + type: list pod_spec_override: description: - A custom Kubernetes or OpenShift Pod specification. @@ -88,7 +88,7 @@ def main(): credential=dict(), policy_instance_percentage=dict(type='int', default='0'), policy_instance_minimum=dict(type='int', default='0'), - policy_instance_list=dict(type='dict'), + policy_instance_list=dict(type='list'), pod_spec_override=dict(), instances=dict(required=False, type="list", default=None), state=dict(choices=['present', 'absent'], default='present'), From 5f29b4bc18ddff260c22eb5e4d5dc3f448c26a8e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 1 Sep 2020 13:17:24 -0400 Subject: [PATCH 9/9] Fixing typo --- awx_collection/test/awx/test_instance_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/test/awx/test_instance_group.py b/awx_collection/test/awx/test_instance_group.py index dc564d8fff..248d6f2d91 100644 --- a/awx_collection/test/awx/test_instance_group.py +++ b/awx_collection/test/awx/test_instance_group.py @@ -44,7 +44,7 @@ def test_instance_group_create(run_module, admin_user): @pytest.mark.django_db -def test_continer_group_create(run_module, admin_user, kube_credential): +def test_container_group_create(run_module, admin_user, kube_credential): pod_spec = "{ 'Nothing': True }" result = run_module('tower_instance_group', {