diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 9406f9082a..6dcbb3610c 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -36,7 +36,7 @@ class TowerModule(AnsibleModule): tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), tower_config_file=dict(type='path', required=False, default=None), ) args.update(argument_spec) @@ -114,7 +114,7 @@ class TowerModule(AnsibleModule): if self.check_mode: self.json_output['changed'] = True self.exit_json(**self.json_output) - + response = self.make_request('POST', endpoint, **kwargs) if response['status_code'] == 201: self.json_output['changed'] = True @@ -135,14 +135,24 @@ class TowerModule(AnsibleModule): response = self.make_request('DELETE', endpoint, **kwargs) if not handle_return: return response - elif response['status_code'] == 204: + elif response['status_code'] in [202, 204]: self.json_output['changed'] = True self.exit_json(**self.json_output) else: self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) def get_all_endpoint(self, endpoint, *args, **kwargs): - raise Exception("This is not implemented") + response = self.get_endpoint(endpoint, *args, **kwargs) + next_page = response['json']['next'] + + if response['json']['count'] > 10000: + self.fail_json(msg='The number of items being queried for is higher than 10,000.') + + while next_page is not None: + next_response = self.get_endpoint(next_page) + response['json']['results'] = response['json']['results'] + next_response['json']['results'] + next_page = next_response['json']['next'] + return response def get_one(self, endpoint, *args, **kwargs): response = self.get_endpoint(endpoint, *args, **kwargs) @@ -165,9 +175,9 @@ class TowerModule(AnsibleModule): if response['json']['count'] == 1: return response['json']['results'][0]['id'] elif response['json']['count'] == 0: - self.fail_json(msg="The {} {} was not found on the Tower server".format(endpoint, name_or_id)) + self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) else: - self.fail_json(msg="Found too many names {} at endpoint {}".format(name_or_id, endpoint)) + self.fail_json(msg="Found too many names {0} at endpoint {1}".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 @@ -179,8 +189,8 @@ class TowerModule(AnsibleModule): endpoint = "/{0}".format(endpoint) if not endpoint.startswith("/api/"): endpoint = "/api/v2{0}".format(endpoint) - if not endpoint.endswith('/'): - endpoint = "{}/".format(endpoint) + if not endpoint.endswith('/') and '?' not in endpoint: + endpoint = "{0}/".format(endpoint) # Extract the headers, this will be used in a couple of places headers = kwargs.get('headers', {}) @@ -191,7 +201,7 @@ class TowerModule(AnsibleModule): self.authenticate(**kwargs) if self.oauth_token: # If we have a oauth toekn we just use a bearer header - headers['Authorization'] = 'Bearer {}'.format(self.oauth_token) + headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token) # Update the URL path with the endpoint self.url = self.url._replace(path=endpoint) @@ -310,8 +320,10 @@ class TowerModule(AnsibleModule): def update_if_needed(self, existing_item, new_item, handle_response=True, **existing_return): for field in new_item: + existing_field = existing_item.get(field, None) + new_field = new_item.get(field, None) # If the two items don't match and we are not comparing '' to None - if existing_item.get(field, None) != new_item.get(field, None) and not (existing_item.get(field, None) is None and new_item.get(field, None) == ''): + if existing_field != new_field and not (existing_field in (None, '') and new_field == ''): # something dosent match so lets do it response = self.patch_endpoint(existing_item['url'], **{'data': new_item}) @@ -332,7 +344,7 @@ class TowerModule(AnsibleModule): self.exit_json(**existing_return) def logout(self): - if self.oauth_token_id != None and self.username and self.password: + if self.oauth_token_id is not None and self.username and self.password: # 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 = (self.url._replace(path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))).geturl() @@ -358,4 +370,3 @@ class TowerModule(AnsibleModule): # Try to logout if we are authenticated self.logout() super().exit_json(**kwargs) - diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 5be7055770..cf44ad9ffe 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -64,6 +64,11 @@ options: default: "present" choices: ["present", "absent"] type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -100,19 +105,20 @@ KIND_CHOICES = { def main(): - - module = TowerModule( - argument_spec = dict( - name=dict(required=True), - description=dict(required=False), - kind=dict(required=False, choices=KIND_CHOICES.keys()), - inputs=dict(type='dict', required=False), - injectors=dict(type='dict', required=False), - state=dict(choices=['present', 'absent'], default='present'), - ), - supports_check_mode=True + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + kind=dict(required=False, choices=KIND_CHOICES.keys()), + inputs=dict(type='dict', required=False), + injectors=dict(type='dict', required=False), + 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 = None kind = module.params.get('kind') @@ -160,5 +166,6 @@ def main(): # This will handle existing on its own module.update_if_needed(credential_type, credental_type_params) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index d2321f3853..56589ac69f 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -59,6 +59,11 @@ options: default: "present" choices: ["present", "absent"] type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -74,30 +79,25 @@ EXAMPLES = ''' ''' -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(), + description=dict(default=''), organization=dict(required=True), - variables=dict(), + variables=dict(default=''), kind=dict(choices=['', 'smart'], default=''), host_filter=dict(), 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') description = module.params.get('description') organization = module.params.get('organization') @@ -106,31 +106,43 @@ def main(): kind = module.params.get('kind') host_filter = module.params.get('host_filter') - json_output = {'inventory': name, 'state': state} + # Attempt to lookup the related items the user specified (these will fail the module if not found) + org_id = module.resolve_name_to_id('organizations', organization) - tower_auth = tower_auth_config(module) - with settings.runtime_values(**tower_auth): - tower_check_mode(module) - inventory = tower_cli.get_resource('inventory') + # Attempt to lookup inventory based on the provided name and org ID + inventory = module.get_one('inventories', **{ + 'data': { + 'name': name, + 'organization': org_id + } + }) - try: - org_res = tower_cli.get_resource('organization') - org = org_res.get(name=organization) + # Create data to sent to create and update + inventory_fields = { + 'name': name, + 'description': description, + 'organization': org_id, + 'variables': variables, + 'kind': kind, + 'host_filter': host_filter, + } - if state == 'present': - result = inventory.modify(name=name, organization=org['id'], variables=variables, - description=description, kind=kind, host_filter=host_filter, - create_on_missing=True) - json_output['id'] = result['id'] - elif state == 'absent': - result = inventory.delete(name=name, organization=org['id']) - except (exc.NotFound) as excinfo: - module.fail_json(msg='Failed to update inventory, organization not found: {0}'.format(excinfo), changed=False) - except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: - module.fail_json(msg='Failed to update inventory: {0}'.format(excinfo), changed=False) - - json_output['changed'] = result['changed'] - module.exit_json(**json_output) + if state == 'absent' and not inventory: + # If the state was absent and we had no inventory, we can just return + module.exit_json(**module.json_output) + elif state == 'absent' and inventory: + # If the state was absent and we had a inventory, we can try to delete it, the module will handle exiting from this + module.delete_endpoint('inventories/{0}'.format(inventory['id']), item_type='inventory', item_name=name, **{}) + elif state == 'present' and not inventory: + # If the state was present and we couldn't find a inventory we can build one, the module will handle exiting from this + module.post_endpoint('inventories', item_type='inventory', item_name=name, **{'data': inventory_fields}) + else: + # Throw a more specific error message than what the API page provides. + if 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 and we had a inventory, we can see if we need to update it + # This will return on its own + module.update_if_needed(inventory, inventory_fields) if __name__ == '__main__': diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 63bd7c6f9a..b079351e0c 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -41,6 +41,11 @@ options: description: - Query used to further filter the list of jobs. C({"foo":"bar"}) will be passed at C(?foo=bar) type: dict + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -81,18 +86,11 @@ results: ''' -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( status=dict(choices=['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful']), page=dict(type='int'), @@ -100,31 +98,35 @@ def main(): query=dict(type='dict'), ) + # Create a module for ourselves module = TowerModule( argument_spec=argument_spec, - supports_check_mode=True + supports_check_mode=True, + mutually_exclusive=[ + ('page', 'all_pages'), + ] ) - json_output = {} - + # Extract our parameters query = module.params.get('query') status = module.params.get('status') page = module.params.get('page') all_pages = module.params.get('all_pages') - tower_auth = tower_auth_config(module) - with settings.runtime_values(**tower_auth): - tower_check_mode(module) - try: - job = tower_cli.get_resource('job') - params = {'status': status, 'page': page, 'all_pages': all_pages} - if query: - params['query'] = query.items() - json_output = job.list(**params) - except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: - module.fail_json(msg='Failed to list jobs: {0}'.format(excinfo), changed=False) + job_search_data = {} + if page: + job_search_data['page'] = page + if status: + job_search_data['status'] = status + if query: + job_search_data.update(query) + if all_pages: + job_list = module.get_all_endpoint('jobs', **{'data': job_search_data}) + else: + job_list = module.get_endpoint('jobs', **{'data': job_search_data}) - module.exit_json(**json_output) + # Attempt to lookup jobs based on the status + module.exit_json(**job_list['json']) if __name__ == '__main__': diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index a8ef96cbda..0aa986cb98 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -14,7 +14,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- -module: license +module: tower_license author: "John Westcott IV (@john-westcott-iv)" version_added: "2.9" short_description: Set the license for Ansible Tower @@ -26,6 +26,17 @@ options: description: - The contents of the license file required: True + type: dict + eula_accepted: + description: + - Whether or not the EULA is accepted. + required: True + type: bool + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -35,6 +46,7 @@ EXAMPLES = ''' - name: Set the license using a file license: data: "{{ lookup('file', '/tmp/my_tower.license') }}" + eula_accepted: True ''' from ..module_utils.tower_api import TowerModule @@ -74,4 +86,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index 4dedd6da70..18c1785173 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -51,14 +51,13 @@ options: default: "present" choices: ["present", "absent"] type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' -# instance_groups: -# description: -# - The name of instance groups to tie to this organization -# type: list -# default: [] -# required: False EXAMPLES = ''' @@ -88,10 +87,21 @@ def main(): description=dict(type='str', required=False), custom_virtualenv=dict(type='str', required=False), max_hosts=dict(type='str', required=False, default="0"), - # instance_groups=dict(type='list', required=False, default=[]), state=dict(type='str', choices=['present', 'absent'], default='present', required=False), ) +# instance_groups=dict(type='list', required=False, default=[]), +# The above argument_spec fragment is being left in for reference since we may need +# it later when finalizing the changes. + +# instance_groups: +# description: +# - The name of instance groups to tie to this organization +# default: [] +# required: False +# The above docstring fragment is being left in for reference since we may need +# it later when finalizing the changes. + # Create a module for ourselves module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) @@ -115,7 +125,7 @@ def main(): } }) - new_org_data = { 'name': name } + new_org_data = {'name': name} if description: new_org_data['description'] = description if custom_virtualenv: @@ -137,7 +147,7 @@ def main(): # 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 wikl handle exiting from this + # 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, }) @@ -146,5 +156,6 @@ def main(): # This will return on its own module.update_if_needed(organization, new_org_data) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index aa5fb4972a..25ec917fec 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -38,6 +38,11 @@ options: - A data structure to be sent into the settings endpoint required: False type: dict + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -66,11 +71,10 @@ EXAMPLES = ''' tower_settings: settings: AUTH_LDAP_BIND_PASSWORD: "password" - AUTH_LDAP_USER_ATTR_MAP: + AUTH_LDAP_USER_ATTR_MAP: email: "mail" first_name: "givenName" last_name: "surname" - ... ''' from ..module_utils.tower_api import TowerModule @@ -78,20 +82,21 @@ from json import loads from json.decoder import JSONDecodeError import re + def main(): # Any additional arguments that are not fields of the item can be added here argument_spec = dict( name=dict(required=False), value=dict(required=False), - settings=dict(required=False,type='dict'), + settings=dict(required=False, type='dict'), ) # Create a module for ourselves module = TowerModule( argument_spec=argument_spec, supports_check_mode=True, - required_one_of=[['name','settings']], - mutually_exclusive=[['name','settings']], + required_one_of=[['name', 'settings']], + mutually_exclusive=[['name', 'settings']], required_if=[['name', 'present', ['value']]] ) @@ -101,22 +106,22 @@ def main(): new_settings = module.params.get('settings') # If we were given a name/value pair we will just make settings out of that and proceed normally - if new_settings == None: + if new_settings is None: new_value = value try: new_value = loads(value) except JSONDecodeError as e: # Attempt to deal with old tower_cli array types if ',' in value: - new_value = re.split(",\s+", new_value) - - new_settings = { name: new_value } + new_value = re.split(r",\s+", new_value) + + new_settings = {name: new_value} # Load the existing settings existing_settings = module.get_endpoint('settings/all')['json'] # Begin a json response - json_response = { 'changed': False, 'old_values': {} } + json_response = {'changed': False, 'old_values': {}} # Check any of the settings to see if anything needs to be updated needs_update = False @@ -124,7 +129,7 @@ def main(): if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]: # At least one thing is different so we need to patch needs_update = True - json_response['old_values'][ a_setting ] = existing_settings[a_setting] + json_response['old_values'][a_setting] = existing_settings[a_setting] # If nothing needs an update we can simply exit with the response (as not changed) if not needs_update: @@ -143,7 +148,7 @@ def main(): new_values[a_setting] = response['json'][a_setting] # If we were using a name we will just add a value of a string, otherwise we will return an array in values - if name != None: + if name is not None: json_response['value'] = new_values[name] else: json_response['values'] = new_values @@ -154,5 +159,6 @@ def main(): else: module.fail_json(**{'msg': "Unable to update settings, see response", 'response': response}) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index 33a78275bc..c0c451ba5b 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -28,6 +28,11 @@ options: - Name to use for the team. required: True type: str + new_name: + description: + - To use when changing a team's name. + required: True + type: str description: description: - The description to use for the team. @@ -43,6 +48,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 ''' @@ -91,6 +101,13 @@ def main(): } }) + # Create data to sent to create and update + team_fields = { + 'name': name, + 'description': description, + 'organization': org_id + } + if state == 'absent' and not team: # If the state was absent and we had no team, we can just return module.exit_json(**module.json_output) @@ -98,23 +115,11 @@ def main(): # If the state was absent and we had a team, we can try to delete it, the module will handle exiting from this module.delete_endpoint('teams/{0}'.format(team['id']), item_type='team', item_name=name, **{}) elif state == 'present' and not team: - # if the state was present and we couldn't find a team we can build one, the module wikl handle exiting from this - module.post_endpoint('teams', item_type='team', item_name=name, **{ - 'data': { - 'name': name, - 'description': description, - 'organization': org_id - } - }) + # if the state was present and we couldn't find a team we can build one, the module will handle exiting from this + module.post_endpoint('teams', item_type='team', item_name=name, **{'data': team_fields}) else: # If the state was present and we had a team we can see if we need to update it # This will return on its own - team_fields = { - 'name': new_name if new_name else name, - 'description': description, - 'organization': org_id, - } - module.update_if_needed(team, team_fields) diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index b63110ae01..c9a628d26a 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -61,7 +61,11 @@ options: default: "present" choices: ["present", "absent"] type: str - + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str extends_documentation_fragment: awx.awx.auth ''' @@ -105,6 +109,7 @@ EXAMPLES = ''' 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( @@ -147,7 +152,7 @@ def main(): # If the state was absent and we had a user, we can try to delete it, the module will handle exiting from this module.delete_endpoint('users/{0}'.format(user['id']), item_type='user', item_name=user_fields['username'], **{}) elif state == 'present' and not user: - # if the state was present and we couldn't find a user we can build one, the module wikl handle exiting from this + # if the state was present and we couldn't find a user we can build one, the module will handle exiting from this module.post_endpoint('users', item_type='user', item_name=user_fields['username'], **{ 'data': user_fields }) @@ -156,5 +161,6 @@ def main(): # This will return on its own module.update_if_needed(user, user_fields) + if __name__ == '__main__': main()