diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 1bc76a84b1..7eee6561d1 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -110,6 +110,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..9a7db2f814 --- /dev/null +++ b/awx_collection/plugins/modules/tower_instance_group.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_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: list + 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='list'), + 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_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'], 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..248d6f2d91 --- /dev/null +++ b/awx_collection/test/awx/test_instance_group.py @@ -0,0 +1,71 @@ +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_container_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"