diff --git a/awx/main/tests/factories/README.md b/awx/main/tests/factories/README.md index c451c02598..916c996cfa 100644 --- a/awx/main/tests/factories/README.md +++ b/awx/main/tests/factories/README.md @@ -52,11 +52,11 @@ patterns -------- `mk` functions are single object fixtures. They should create only a single object with the minimum deps. -They should also accept a `persited` flag, if they must be persisted to work, they raise an error if persisted=False +They should also accept a `persisted` flag, if they must be persisted to work, they raise an error if persisted=False `generate` and `apply` functions are helpers that build up the various parts of a `create` functions objects. These should be useful for more than one create function to use and should explicitly accept all of the values needed -to execute. These functions should also be robust and have very speciifc error reporting about constraints and/or +to execute. These functions should also be robust and have very specific error reporting about constraints and/or bad values. `create` functions compose many of the `mk` and `generate` functions to make different object diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 4755670291..1714773b32 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -128,6 +128,10 @@ options: - list of notifications to send on error type: list elements: str + organization: + description: + - Name of the inventory source's inventory's organization. + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -140,6 +144,7 @@ EXAMPLES = ''' credential: previously-created-credential overwrite: True update_on_launch: True + organization: Default source_vars: private: false ''' @@ -168,6 +173,7 @@ def main(): enabled_value=dict(), host_filter=dict(), credential=dict(), + organization=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), custom_virtualenv=dict(), @@ -190,23 +196,30 @@ def main(): name = module.params.get('name') new_name = module.params.get('new_name') inventory = module.params.get('inventory') + organization = module.params.get('organization') source_script = module.params.get('source_script') credential = module.params.get('credential') source_project = module.params.get('source_project') state = module.params.get('state') - # Attempt to look up inventory source based on the provided name and inventory ID - inventory_id = module.resolve_name_to_id('inventories', inventory) - inventory_source = module.get_one('inventory_sources', **{ + lookup_data = {'name': inventory} + if organization: + lookup_data['organization'] = module.resolve_name_to_id('organizations', organization) + inventory_object = module.get_one('inventories', data=lookup_data) + + if not inventory_object: + module.fail_json(msg='The specified inventory, {0}, was not found.'.format(lookup_data)) + + inventory_source_object = module.get_one('inventory_sources', **{ 'data': { 'name': name, - 'inventory': inventory_id, + 'inventory': inventory_object['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(inventory_source) + module.delete_if_needed(inventory_source_object) # Attempt to look up associated field items the user specified. association_fields = {} @@ -232,7 +245,7 @@ def main(): # Create the data that gets sent for create and update inventory_source_fields = { 'name': new_name if new_name else name, - 'inventory': inventory_id, + 'inventory': inventory_object['id'], } # Attempt to look up the related items the user specified (these will fail the module if not found) @@ -261,12 +274,12 @@ def main(): inventory_source_fields['source_vars'] = dumps(inventory_source_fields['source_vars']) # Sanity check on arguments - if state == 'present' and not inventory_source and not inventory_source_fields['source']: + if state == 'present' and not inventory_source_object and not inventory_source_fields['source']: module.fail_json(msg="If creating a new inventory source, the source param must be present") - # If the state was present we can let the module build or update the existing inventory_source, this will return on its own + # If the state was present we can let the module build or update the existing inventory_source_object, this will return on its own module.create_or_update_if_needed( - inventory_source, inventory_source_fields, + inventory_source_object, inventory_source_fields, endpoint='inventory_sources', item_type='inventory source', associations=association_fields ) diff --git a/awx_collection/plugins/modules/tower_inventory_source_update.py b/awx_collection/plugins/modules/tower_inventory_source_update.py new file mode 100644 index 0000000000..c4cb27be7c --- /dev/null +++ b/awx_collection/plugins/modules/tower_inventory_source_update.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, Bianca Henderson +# 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_inventory_source_update +author: "Bianca Henderson (@beeankha)" +short_description: Update inventory source(s). +description: + - Update Ansible Tower inventory source(s). See + U(https://www.ansible.com/tower) for an overview. +options: + inventory: + description: + - Name of the inventory that contains the inventory source(s) to update. + required: True + type: str + inventory_source: + description: + - The name of the inventory source to update. + required: True + type: str + organization: + description: + - Name of the inventory source's inventory's organization. + type: str + wait: + description: + - Wait for the job to complete. + default: False + type: bool + interval: + description: + - The interval to request an update from Tower. + required: False + default: 1 + type: float + timeout: + description: + - If waiting for the job to complete this will abort after this + amount of seconds + type: int +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Update a single inventory source + tower_inventory_source_update: + inventory: "My Inventory" + inventory_source: "Example Inventory Source" + organization: Default + +- name: Update all inventory sources + tower_inventory_source_update: + inventory: "My Other Inventory" + inventory_source: "{{ item }}" + loop: "{{ query('awx.awx.tower_api', 'inventory_sources', query_params={ 'inventory': 30 }, return_ids=True ) }}" +''' + +RETURN = ''' +id: + description: id of the inventory update + returned: success + type: int + sample: 86 +status: + description: status of the inventory update + returned: success + type: str + sample: pending +''' + +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( + inventory=dict(required=True), + inventory_source=dict(required=True), + organization=dict(), + wait=dict(default=False, type='bool'), + interval=dict(default=1.0, type='float'), + timeout=dict(default=None, type='int'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + inventory = module.params.get('inventory') + inventory_source = module.params.get('inventory_source') + organization = module.params.get('organization') + wait = module.params.get('wait') + interval = module.params.get('interval') + timeout = module.params.get('timeout') + + lookup_data = {'name': inventory} + if organization: + lookup_data['organization'] = module.resolve_name_to_id('organizations', organization) + inventory_object = module.get_one('inventories', data=lookup_data) + + if not inventory_object: + module.fail_json(msg='The specified inventory, {0}, was not found.'.format(lookup_data)) + + inventory_source_object = module.get_one('inventory_sources', **{ + 'data': { + 'name': inventory_source, + 'inventory': inventory_object['id'], + } + }) + + if not inventory_source_object: + module.fail_json(msg='The specified inventory source was not found.') + + # Sync the inventory source(s) + inventory_source_update_results = module.post_endpoint(inventory_source_object['related']['update'], **{'data': {}}) + + if inventory_source_update_results['status_code'] != 202: + module.fail_json(msg="Failed to update inventory source, see response for details", **{'response': inventory_source_update_results}) + + module.json_output['changed'] = True + module.json_output['id'] = inventory_source_update_results['json']['id'] + module.json_output['status'] = inventory_source_update_results['json']['status'] + + if not wait: + module.exit_json(**module.json_output) + + # Invoke wait function + module.wait_on_url( + url=inventory_source_update_results['json']['url'], + object_name=inventory_object, + object_type='inventory_update', + timeout=timeout, interval=interval + ) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 2f1fc80d4d..ab8c1547f8 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -23,9 +23,9 @@ no_module_for_endpoint = [] # Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint no_endpoint_for_module = [ - 'tower_import', 'tower_meta', 'tower_export', 'tower_job_launch', 'tower_job_wait', 'tower_job_list', - 'tower_license', 'tower_ping', 'tower_receive', 'tower_send', 'tower_workflow_launch', 'tower_job_cancel', - 'tower_workflow_template', + 'tower_import', 'tower_meta', 'tower_export', 'tower_inventory_source_update', 'tower_job_launch', 'tower_job_wait', + 'tower_job_list', 'tower_license', 'tower_ping', 'tower_receive', 'tower_send', 'tower_workflow_launch', + 'tower_job_cancel', 'tower_workflow_template', ] # Global module parameters we can ignore @@ -43,7 +43,8 @@ no_api_parameter_ok = { # /survey spec is now how we handle associations # We take an organization here to help with the lookups only 'tower_job_template': ['survey_spec', 'organization'], - # Organization is how we looking job templates + 'tower_inventory_source': ['organization'], + # Organization is how we are looking up job templates 'tower_workflow_job_template_node': ['organization'], # Survey is how we handle associations 'tower_workflow_job_template': ['survey'], diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index b27653fb94..9f52574670 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -80,7 +80,8 @@ def test_create_inventory_source_multiple_orgs(run_module, admin_user): result = run_module('tower_inventory_source', dict( name='Test Inventory Source', - inventory=inv2.id, + inventory=inv2.name, + organization='test-org-number-two', source='ec2', state='present' ), admin_user) diff --git a/awx_collection/tests/integration/targets/tower_inventory_source_update/tasks/main.yml b/awx_collection/tests/integration/targets/tower_inventory_source_update/tasks/main.yml new file mode 100644 index 0000000000..36a4e4b058 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_inventory_source_update/tasks/main.yml @@ -0,0 +1,116 @@ +--- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Generate names + set_fact: + project_name: "AWX-Collection-tests-tower_inventory_source_update-project-{{ test_id }}" + inv_name: "AWX-Collection-tests-tower_inventory_source_update-inv-{{ test_id }}" + inv_source1: "AWX-Collection-tests-tower_inventory_source_update-source1-{{ test_id }}" + inv_source2: "AWX-Collection-tests-tower_inventory_source_update-source2-{{ test_id }}" + inv_source3: "AWX-Collection-tests-tower_inventory_source_update-source3-{{ test_id }}" + org_name: "AWX-Collection-tests-tower_inventory_source_update-org-{{ test_id }}" + + +- block: + + - name: "Create a new organization" + tower_organization: + name: "{{ org_name }}" + register: created_org + + - name: Create a git project without credentials + tower_project: + name: "{{ project_name }}" + organization: "{{ org_name }}" + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + wait: true + + - name: Create an Inventory + tower_inventory: + name: "{{ inv_name }}" + organization: "{{ org_name }}" + state: present + + - name: Create another inventory w/ same name, different org + tower_inventory: + name: "{{ inv_name }}" + organization: Default + state: present + register: created_inventory + + - name: Create an Inventory Source (specifically connected to the randomly generated org) + tower_inventory_source: + name: "{{ inv_source1 }}" + source: scm + source_project: "{{ project_name }}" + source_path: inventories/inventory.ini + description: Source for Test inventory + organization: "{{ created_org.id }}" + inventory: "{{ inv_name }}" + + - name: Create Another Inventory Source + tower_inventory_source: + name: "{{ inv_source2 }}" + source: scm + source_project: "{{ project_name }}" + source_path: inventories/create_10_hosts.ini + description: Source for Test inventory + organization: Default + inventory: "{{ inv_name }}" + + - name: Create Yet Another Inventory Source (to make lookup plugin find multiple inv sources) + tower_inventory_source: + name: "{{ inv_source3 }}" + source: scm + source_project: "{{ project_name }}" + source_path: inventories/create_100_hosts.ini + description: Source for Test inventory + organization: Default + inventory: "{{ inv_name }}" + + - name: Test Inventory Source Update + tower_inventory_source_update: + inventory: "{{ inv_name }}" + inventory_source: "{{ inv_source2 }}" + organization: Default + register: result + + - assert: + that: + - "result is changed" + + - name: Test Inventory Source Update for All Sources + tower_inventory_source_update: + inventory: "{{ inv_name }}" + inventory_source: "{{ item.name }}" + organization: Default + wait: true + loop: "{{ query('awx.awx.tower_api', 'inventory_sources', query_params={ 'inventory': created_inventory.id }, expect_objects=True, return_objects=True) }}" + loop_control: + label: "{{ item.name }}" + register: result + + - assert: + that: + - "result is changed" + + always: + - name: Delete Inventory + tower_inventory: + name: "{{ inv_name }}" + organization: Default + state: absent + + - name: Delete Project + tower_project: + name: "{{ project_name }}" + organization: Default + state: absent + + - name: "Remove the organization" + tower_organization: + name: "{{ org_name }}" + state: absent