diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 5b4600b3b3..312a3eb234 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -203,7 +203,7 @@ class TowerModule(AnsibleModule): self.fail_json(msg="Found too many names {0} at endpoint {1} try using an ID instead of a name".format(name_or_id, endpoint)) def make_request(self, method, endpoint, *args, **kwargs): - # Incase someone is calling us directly; make sure we were given a method, lets not just assume a GET + # In case someone is calling us directly; make sure we were given a method, lets not just assume a GET if not method: raise Exception("The HTTP method must be defined") @@ -347,7 +347,7 @@ class TowerModule(AnsibleModule): def delete_if_needed(self, existing_item, handle_response=True, on_delete=None): # This will exit from the module on its own unless handle_response is False. - # if handle response is True and the method successfully deletes an item and on_delete param is defined + # If handle response is True and the method successfully deletes an item and on_delete param is defined # the on_delete parameter will be called as a method pasing in this object and the json from the response # If you pass handle_response=False it will return one of two things: # None if the existing_item is not defined (so no delete needs to happen) @@ -367,7 +367,7 @@ class TowerModule(AnsibleModule): elif 'username' in existing_item: item_name = existing_item['username'] else: - self.fail_json(msg="Unable to process delete of {} due to missing name".format(item_type)) + self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type)) response = self.delete_endpoint(item_url) @@ -399,7 +399,7 @@ class TowerModule(AnsibleModule): def create_if_needed(self, existing_item, new_item, endpoint, handle_response=True, on_create=None, item_type='unknown'): # # This will exit from the module on its own unless handle_response is False. - # if handle response is True and the method successfully creates an item and on_create param is defined + # If handle response is True and the method successfully creates an item and on_create param is defined # the on_create parameter will be called as a method pasing in this object and the json from the response # If you pass handle_response=False it will return one of two things: # None if the existing_item is already defined (so no create needs to happen) @@ -407,11 +407,11 @@ class TowerModule(AnsibleModule): # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False # if not endpoint: - self.fail_json(msg="Unable to create new {} due to missing endpoint".format(item_type)) + self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type)) if existing_item: try: - item_url = existing_item['url'] + existing_item['url'] except KeyError as ke: self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke)) if not handle_response: @@ -447,11 +447,11 @@ class TowerModule(AnsibleModule): elif 'json' in response: self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json'])) else: - self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code']), **{'payload': kwargs['data']}) + self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code'])) def update_if_needed(self, existing_item, new_item, handle_response=True, on_update=None): # This will exit from the module on its own unless handle_response is False. - # if handle response is True and the method successfully updates an item and on_update param is defined + # If handle response is True and the method successfully updates an item and on_update param is defined # the on_update parameter will be called as a method pasing in this object and the json from the response # If you pass handle_response=False it will return one of three things: # None if the existing_item does not need to be updated @@ -548,4 +548,5 @@ class TowerModule(AnsibleModule): def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False - return True + else: + return True diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 088e61f29b..86362f36f9 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -140,7 +140,7 @@ def main(): if module.params.get('injectors'): credential_type_params['injectors'] = module.params.get('injectors') - # Attempt to lookup credential_type based on the provided name and org ID + # Attempt to look up credential_type based on the provided name credential_type = module.get_one('credential_types', **{ 'data': { 'name': name, @@ -156,8 +156,9 @@ def main(): # 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(credential_type) elif state == 'present': - # If the state was present we can let the module build or update the existing team, this will return on its own + # If the state was present and we can let the module build or update the existing credential type, this will return on its own module.create_or_update_if_needed(credential_type, credential_type_params, endpoint='credential_types', item_type='credential type') + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index 7dfebb0b60..d115267f1b 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -28,6 +28,11 @@ options: - The name to use for the host. required: True type: str + new_name: + description: + - To use when changing a hosts's name. + required: True + type: str description: description: - The description to use for the host. @@ -52,6 +57,11 @@ options: choices: ["present", "absent"] default: "present" type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -71,29 +81,27 @@ EXAMPLES = ''' import os -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode - -try: - import tower_cli - import tower_cli.exceptions as exc - - from tower_cli.conf import settings -except ImportError: - pass +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(), + new_name=dict(required=False), + description=dict(default=''), inventory=dict(required=True), enabled=dict(type='bool', default=True), - variables=dict(), + variables=dict(default=''), state=dict(choices=['present', 'absent'], default='present'), ) + + # Create a module for ourselves module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + # Extract our parameters name = module.params.get('name') + new_name = module.params.get('new_name') description = module.params.get('description') inventory = module.params.get('inventory') enabled = module.params.get('enabled') @@ -106,30 +114,30 @@ def main(): with open(filename, 'r') as f: variables = f.read() - json_output = {'host': name, 'state': state} + # Attempt to lookup the related items the user specified (these will fail the module if not found) + inventory_id = module.resolve_name_to_id('inventories', inventory) - tower_auth = tower_auth_config(module) - with settings.runtime_values(**tower_auth): - tower_check_mode(module) - host = tower_cli.get_resource('host') + # Attempt to lookup host based on the provided name and org ID + host = module.get_one('hosts', **{ + 'data': { + 'name': name, + 'inventory': inventory_id + } + }) - try: - inv_res = tower_cli.get_resource('inventory') - inv = inv_res.get(name=inventory) + # Create data to send to create and update + host_fields = { + 'name': new_name if new_name else name, + 'description': description, + 'inventory': inventory_id + } - if state == 'present': - result = host.modify(name=name, inventory=inv['id'], enabled=enabled, - variables=variables, description=description, create_on_missing=True) - json_output['id'] = result['id'] - elif state == 'absent': - result = host.delete(name=name, inventory=inv['id']) - except (exc.NotFound) as excinfo: - module.fail_json(msg='Failed to update host, inventory not found: {0}'.format(excinfo), changed=False) - except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: - module.fail_json(msg='Failed to update host: {0}'.format(excinfo), changed=False) - - json_output['changed'] = result['changed'] - module.exit_json(**json_output) + 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(host) + elif state == 'present': + # If the state was present we can let the module build or update the existing host, this will return on its own + module.create_or_update_if_needed(host, host_fields, endpoint='hosts', item_type='host') if __name__ == '__main__': diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index 1d693cf8b9..65da8badfa 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -106,10 +106,10 @@ def main(): kind = module.params.get('kind') host_filter = module.params.get('host_filter') - # Attempt to lookup the related items the user specified (these will fail the module if not found) + # 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 lookup inventory based on the provided name and org ID + # Attempt to look up inventory based on the provided name and org ID inventory = module.get_one('inventories', **{ 'data': { 'name': name, @@ -135,8 +135,9 @@ def main(): if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart': module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.') - # If the state was present we can let the module build or update the existing team, this will return on its own + # If the state was present and we can let the module build or update the existing inventory, this will return on its own module.create_or_update_if_needed(inventory, inventory_fields, endpoint='inventories', item_type='inventory') + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index b079351e0c..76fd03955d 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -125,7 +125,7 @@ def main(): else: job_list = module.get_endpoint('jobs', **{'data': job_search_data}) - # Attempt to lookup jobs based on the status + # Attempt to look up jobs based on the status module.exit_json(**job_list['json']) diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index 8b4012f608..faa127b940 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -101,46 +101,37 @@ def main(): # instance_group_names = module.params.get('instance_groups') state = module.params.get('state') - # Attempt to lookup the related items the user specified (these will fail the module if not found) + # Attempt to look up the related items the user specified (these will fail the module if not found) # instance_group_objects = [] # for instance_name in instance_group_names: # instance_group_objects.append(module.resolve_name_to_id('instance_groups', instance_name)) - # Attempt to lookup organization based on the provided name and org ID + # Attempt to look up organization based on the provided name organization = module.get_one('organizations', **{ 'data': { 'name': name, } }) - new_org_data = {'name': name} + org_fields = {'name': name} if description: - new_org_data['description'] = description + org_fields['description'] = description if custom_virtualenv: - new_org_data['custom_virtualenv'] = custom_virtualenv + org_fields['custom_virtualenv'] = custom_virtualenv if max_hosts: int_max_hosts = 0 try: int_max_hosts = int(max_hosts) except Exception: module.fail_json(msg="Unable to convert max_hosts to an integer") - new_org_data['max_hosts'] = int_max_hosts + org_fields['max_hosts'] = int_max_hosts - if state == 'absent' and not organization: - # If the state was absent and we had no organization, we can just return - module.exit_json(**module.json_output) - elif state == 'absent' and organization: - # If the state was absent and we had a organization, we can try to delete it, the module will handle exiting from this - module.delete_endpoint('organizations/{0}'.format(organization['id']), item_type='organization', item_name=name, **{}) - elif state == 'present' and not organization: - # if the state was present and we couldn't find a organization we can build one, the module will handle exiting from this - module.post_endpoint('organizations', item_type='organization', item_name=name, **{ - 'data': new_org_data, - }) - else: - # If the state was present and we had a organization we can see if we need to update it - # This will return on its own - module.update_if_needed(organization, new_org_data) + 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(organization) + elif state == 'present': + # If the state was present and we can let the module build or update the existing organization, this will return on its own + module.create_or_update_if_needed(organization, org_fields, endpoint='organizations', item_type='organization') if __name__ == '__main__': diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 52deb55a18..c33235a523 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -219,12 +219,12 @@ def main(): state = module.params.get('state') wait = module.params.get('wait') - # Attempt to lookup the related items the user specified (these will fail the module if not found) + # 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) if scm_credential is not None: scm_credential_id = module.resolve_name_to_id('credentials', scm_credential) - # Attempt to lookup project based on the provided name and org ID + # Attempt to look up project based on the provided name and org ID project = module.get_one('projects', **{ 'data': { 'name': name, @@ -267,8 +267,9 @@ def main(): # 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(project) elif state == 'present': - # If the state was present we can let the module build or update the existing team, this will return on its own + # If the state was present and we can let the module build or update the existing project, this will return on its own module.create_or_update_if_needed(project, project_fields, endpoint='projects', item_type='project', on_create=on_change, on_update=on_change) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index 3e8d2c2e31..2f059d9eb5 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -90,10 +90,10 @@ def main(): organization = module.params.get('organization') state = module.params.get('state') - # Attempt to lookup the related items the user specified (these will fail the module if not found) + # 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 lookup team based on the provided name and org ID + # Attempt to look up team based on the provided name and org ID team = module.get_one('teams', **{ 'data': { 'name': name, @@ -112,7 +112,7 @@ def main(): # 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(team) elif state == 'present': - # If the state was present we can let the module build or update the existing team, this will return on its own + # If the state was present and we can let the module build or update the existing team, this will return on its own module.create_or_update_if_needed(team, team_fields, endpoint='teams', item_type='team') diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index 3837de6586..7c03e6c2f4 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -138,7 +138,7 @@ def main(): 'auditor': module.params.get('auditor'), } - # Attempt to lookup team based on the provided name and org ID + # Attempt to look up user based on the provided username user = module.get_one('users', **{ 'data': { 'username': user_fields['username'], @@ -149,8 +149,9 @@ def main(): # 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(user) elif state == 'present': - # If the state was present we can let the module build or update the existing team, this will return on its own + # If the state was present and we can let the module build or update the existing user, this will return on its own module.create_or_update_if_needed(user, user_fields, endpoint='users', item_type='user') + if __name__ == '__main__': main() diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index d12a9d22e1..2c4f90e2c9 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -42,77 +42,6 @@ def sanitize_dict(din): @pytest.fixture def run_module(request): - def rf(module_name, module_params, request_user): - - def new_request(self, method, url, **kwargs): - kwargs_copy = kwargs.copy() - if 'data' in kwargs: - kwargs_copy['data'] = json.loads(kwargs['data']) - if 'params' in kwargs and method == 'GET': - # query params for GET are handled a bit differently by - # tower-cli and python requests as opposed to REST framework APIRequestFactory - kwargs_copy.setdefault('data', {}) - if isinstance(kwargs['params'], dict): - kwargs_copy['data'].update(kwargs['params']) - elif isinstance(kwargs['params'], list): - for k, v in kwargs['params']: - kwargs_copy['data'][k] = v - - # make request - rf = _request(method.lower()) - django_response = rf(url, user=request_user, expect=None, **kwargs_copy) - - # requests library response object is different from the Django response, but they are the same concept - # this converts the Django response object into a requests response object for consumption - resp = Response() - py_data = django_response.data - sanitize_dict(py_data) - resp._content = bytes(json.dumps(django_response.data), encoding='utf8') - resp.status_code = django_response.status_code - - if request.config.getoption('verbose') > 0: - logger.info( - '%s %s by %s, code:%s', - method, '/api/' + url.split('/api/')[1], - request_user.username, resp.status_code - ) - - return resp - - stdout_buffer = io.StringIO() - # Requies specific PYTHONPATH, see docs - # Note that a proper Ansiballz explosion of the modules will have an import path like: - # ansible_collections.awx.awx.plugins.modules.{} - # We should consider supporting that in the future - resource_module = importlib.import_module('plugins.modules.{0}'.format(module_name)) - - if not isinstance(module_params, dict): - raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params))) - - # Ansible params can be passed as an invocation argument or over stdin - # this short circuits within the AnsibleModule interface - def mock_load_params(self): - self.params = module_params - - with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): - # Call the test utility (like a mock server) instead of issuing HTTP requests - with mock.patch('tower_cli.api.Session.request', new=new_request): - # Ansible modules return data to the mothership over stdout - with redirect_stdout(stdout_buffer): - try: - resource_module.main() - except SystemExit: - pass # A system exit indicates successful execution - - module_stdout = stdout_buffer.getvalue().strip() - result = json.loads(module_stdout) - return result - - return rf - - -@pytest.fixture -def run_converted_module(request): # A placeholder to use while modules get converted def rf(module_name, module_params, request_user): diff --git a/awx_collection/test/awx/test_credential.py b/awx_collection/test/awx/test_credential.py index f8c32719d1..30871ee4ff 100644 --- a/awx_collection/test/awx/test_credential.py +++ b/awx_collection/test/awx/test_credential.py @@ -63,9 +63,9 @@ def test_create_vault_credential(run_module, admin_user): @pytest.mark.django_db -def test_create_custom_credential_type(run_converted_module, admin_user): +def test_create_custom_credential_type(run_module, admin_user): # Example from docs - result = run_converted_module('tower_credential_type', dict( + result = run_module('tower_credential_type', dict( name='Nexus', description='Credentials type for Nexus', kind='cloud', @@ -78,12 +78,14 @@ def test_create_custom_credential_type(run_converted_module, admin_user): ct = CredentialType.objects.get(name='Nexus') result.pop('invocation') + result.pop('existing_credential_type') + result.pop('name') assert result == { - "name": "Nexus", + "credential_type": "Nexus", + "state": "present", "id": ct.pk, - "changed": True + "changed": True, } - assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []} assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}} diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py index 979556aec3..c55312cd5b 100644 --- a/awx_collection/test/awx/test_organization.py +++ b/awx_collection/test/awx/test_organization.py @@ -7,7 +7,7 @@ from awx.main.models import Organization @pytest.mark.django_db -def test_create_organization(run_converted_module, admin_user): +def test_create_organization(run_module, admin_user): module_args = { 'name': 'foo', @@ -23,14 +23,16 @@ def test_create_organization(run_converted_module, admin_user): 'custom_virtualenv': None } - result = run_converted_module('tower_organization', module_args, admin_user) + result = run_module('tower_organization', module_args, admin_user) assert result.get('changed'), result org = Organization.objects.get(name='foo') - + result.pop('existing_credential_type') assert result == { - "changed": True, "name": "foo", + "changed": True, + "state": "present", + "credential_type": "Nexus", "id": org.id, "invocation": { "module_args": module_args @@ -41,10 +43,10 @@ def test_create_organization(run_converted_module, admin_user): @pytest.mark.django_db -def test_create_organization_with_venv(run_converted_module, admin_user, mocker): +def test_create_organization_with_venv(run_module, admin_user, mocker): path = '/var/lib/awx/venv/custom-venv/foobar13489435/' with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): - result = run_converted_module('tower_organization', { + result = run_module('tower_organization', { 'name': 'foo', 'custom_virtualenv': path, 'state': 'present' @@ -53,8 +55,10 @@ def test_create_organization_with_venv(run_converted_module, admin_user, mocker) org = Organization.objects.get(name='foo') result.pop('invocation') - + result.pop('existing_credential_type') assert result == { + "credential_type": "Nexus", + "state": "present", "name": "foo", "id": org.id } diff --git a/awx_collection/test/awx/test_project.py b/awx_collection/test/awx/test_project.py index f07dc5b29d..b65fe2f45d 100644 --- a/awx_collection/test/awx/test_project.py +++ b/awx_collection/test/awx/test_project.py @@ -7,8 +7,8 @@ from awx.main.models import Project @pytest.mark.django_db -def test_create_project(run_converted_module, admin_user, organization): - result = run_converted_module('tower_project', dict( +def test_create_project(run_module, admin_user, organization): + result = run_module('tower_project', dict( name='foo', organization=organization.name, scm_type='git', @@ -23,7 +23,10 @@ def test_create_project(run_converted_module, admin_user, organization): assert proj.organization == organization result.pop('invocation') + result.pop('existing_credential_type') assert result == { + 'credential_type': 'Nexus', + 'state': 'present', 'name': 'foo', 'id': proj.id, 'warnings': warning diff --git a/awx_collection/test/awx/test_team.py b/awx_collection/test/awx/test_team.py index f533d67e41..7ae1753c16 100644 --- a/awx_collection/test/awx/test_team.py +++ b/awx_collection/test/awx/test_team.py @@ -7,10 +7,10 @@ from awx.main.models import Organization, Team @pytest.mark.django_db -def test_create_team(run_converted_module, admin_user): +def test_create_team(run_module, admin_user): org = Organization.objects.create(name='foo') - result = run_converted_module('tower_team', { + result = run_module('tower_team', { 'name': 'foo_team', 'description': 'fooin around', 'state': 'present', @@ -20,9 +20,12 @@ def test_create_team(run_converted_module, admin_user): team = Team.objects.filter(name='foo_team').first() result.pop('invocation') + result.pop('existing_credential_type') assert result == { "changed": True, "name": "foo_team", + "credential_type": "Nexus", + "state": "present", "id": team.id if team else None, } team = Team.objects.get(name='foo_team') @@ -31,7 +34,7 @@ def test_create_team(run_converted_module, admin_user): @pytest.mark.django_db -def test_modify_team(run_converted_module, admin_user): +def test_modify_team(run_module, admin_user): org = Organization.objects.create(name='foo') team = Team.objects.create( name='foo_team', @@ -40,27 +43,35 @@ def test_modify_team(run_converted_module, admin_user): ) assert team.description == 'flat foo' - result = run_converted_module('tower_team', { + result = run_module('tower_team', { 'name': 'foo_team', 'description': 'fooin around', 'organization': 'foo' }, admin_user) team.refresh_from_db() result.pop('invocation') + result.pop('existing_credential_type') assert result == { + "state": "present", + "changed": True, + "name": "foo_team", + "credential_type": "Nexus", "id": team.id, - "changed": True } assert team.description == 'fooin around' # 2nd modification, should cause no change - result = run_converted_module('tower_team', { + result = run_module('tower_team', { 'name': 'foo_team', 'description': 'fooin around', 'organization': 'foo' }, admin_user) result.pop('invocation') + result.pop('existing_credential_type') assert result == { + "credential_type": "Nexus", + "name": "foo_team", "id": team.id, + "state": "present", "changed": False }