From c340fff64386b220805b391d496f1ae21d4a0402 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 11 Mar 2020 10:17:43 -0400 Subject: [PATCH 1/6] Add generator playbook for the AWX Collection modules, along with other module generation tools --- awx_collection/galaxy.yml.j2 | 1 + awx_collection/tools/ansible.cfg | 2 + awx_collection/tools/generate.yml | 82 ++++++++ .../tools/templates/tower_module.j2 | 197 ++++++++++++++++++ awx_collection/tools/vars/aliases.yml | 29 +++ awx_collection/tools/vars/examples.yml | 52 +++++ awx_collection/tools/vars/generate_for.yml | 15 ++ 7 files changed, 378 insertions(+) create mode 100644 awx_collection/tools/ansible.cfg create mode 100644 awx_collection/tools/generate.yml create mode 100644 awx_collection/tools/templates/tower_module.j2 create mode 100644 awx_collection/tools/vars/aliases.yml create mode 100644 awx_collection/tools/vars/examples.yml create mode 100644 awx_collection/tools/vars/generate_for.yml diff --git a/awx_collection/galaxy.yml.j2 b/awx_collection/galaxy.yml.j2 index 87bdc97ae6..b2a00538d7 100644 --- a/awx_collection/galaxy.yml.j2 +++ b/awx_collection/galaxy.yml.j2 @@ -19,6 +19,7 @@ tags: - automation version: {{ collection_version }} build_ignore: +- tools - setup.cfg - galaxy.yml.j2 - template_galaxy.yml diff --git a/awx_collection/tools/ansible.cfg b/awx_collection/tools/ansible.cfg new file mode 100644 index 0000000000..3efd4c3830 --- /dev/null +++ b/awx_collection/tools/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +stdout_callback = yaml diff --git a/awx_collection/tools/generate.yml b/awx_collection/tools/generate.yml new file mode 100644 index 0000000000..44772239cc --- /dev/null +++ b/awx_collection/tools/generate.yml @@ -0,0 +1,82 @@ +--- +- name: Generate the awx.awx collection + hosts: localhost + connection: local + gather_facts: False + vars: + api_url: "{{ lookup('env', 'TOWER_HOST') }}" + vars_files: + - vars/generate_for.yml + - vars/aliases.yml + - vars/examples.yml + module_defaults: + uri: + validate_certs: False + force_basic_auth: True + url_username: "{{ lookup('env', 'TOWER_USERNAME') }}" + url_password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + + tasks: + - name: Get date time data + setup: + gather_subset: min + + - name: Create module directory + file: + state: directory + name: "modules" + + - name: Load api/v2 + uri: + method: GET + url: "{{ api_url }}/api/v2/" + register: endpoints + + - name: Load endpoint options + uri: + method: "OPTIONS" + url: "{{ api_url }}{{ item.value }}" + loop: "{{ endpoints['json'] | dict2items }}" + loop_control: + label: "{{ item.key }}" + register: end_point_options + when: "generate_for is not defined or item.key in generate_for" + + - name: Scan POST options for different things + set_fact: + all_options: "{{ all_options | default({}) | combine(options[0]) }}" + loop: "{{ end_point_options.results }}" + vars: + options: "{{ item | json_query('json.actions.POST.[*]') }}" + loop_control: + label: "{{ item['item']['key'] }}" + when: + - item is not skipped + - options is defined + + - name: Process endpoint + template: + src: "templates/tower_module.j2" + dest: "modules/{{ file_name }}" + loop: "{{ end_point_options['results'] }}" + loop_control: + label: "{{ item['item']['key'] }}" + when: "'json' in item and 'actions' in item['json'] and 'POST' in item['json']['actions']" + vars: + item_type: "{{ item['item']['key'] }}" + human_readable: "{{ item_type | replace('_', ' ') }}" + singular_item_type: "{{ item['item']['key'] | regex_replace('ies$', 'y') | regex_replace('s$', '') }}" + file_name: "tower_{% if item['item']['key'] in ['settings'] %}{{ item['item']['key'] }}{% else %}{{ singular_item_type }}{% endif %}.py" + type_map: + bool: 'bool' + boolean: 'bool' + choice: 'str' + datetime: 'str' + id: 'str' + int: 'int' + integer: 'int' + json: 'dict' + list: 'list' + object: 'dict' + password: 'str' + string: 'str' diff --git a/awx_collection/tools/templates/tower_module.j2 b/awx_collection/tools/templates/tower_module.j2 new file mode 100644 index 0000000000..321901a574 --- /dev/null +++ b/awx_collection/tools/templates/tower_module.j2 @@ -0,0 +1,197 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +{# The following is set by the generate.yml file: + # item_type: the type of item i.e. 'teams' + # human_readable: the type with _ replaced with spaces i.e. worflow job template + # singular_item_type: the type of an item replace singularized i.e. team + # type_map: a mapping of things like string to str + #} +{% set name_option = 'username' if item_type == 'users' else 'name' %} + +# (c) {{ ansible_date_time['year'] }}, 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_{{ singular_item_type }} +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower {{ human_readable }}. +description: + - Create, update, or destroy Ansible Tower {{ human_readable }}. See + U(https://www.ansible.com/tower) for an overview. +options: +{% for option in item['json']['actions']['POST'] %} +{# to do: sort documentation options #} + {{ option }}: + description: +{% if 'help_text' in item['json']['actions']['POST'][option] %} + - {{ item['json']['actions']['POST'][option]['help_text'] }} +{% else %} + - NO DESCRIPTION GIVEN IN THE TOWER API +{% endif %} + required: {{ item['json']['actions']['POST'][option]['required'] }} + type: {{ type_map[ item['json']['actions']['POST'][option]['type'] ] }} +{% if 'default' in item['json']['actions']['POST'][option] %} + default: '{{ item['json']['actions']['POST'][option]['default'] }}' +{% endif %} +{% if 'choices' in item['json']['actions']['POST'][option] %} + choices: +{% for choice in item['json']['actions']['POST'][option]['choices'] %} + - '{{ choice[0] }}' +{% endfor %} +{%endif %} +{% if aliases[item_type][option] | default(False) %} + aliases: +{% for alias_name in aliases[item_type][option] %} + - {{ alias_name }} +{% endfor %} +{% endif %} +{% if option == name_option %} + new_{{ name_option }}: + description: + - Setting this option will change the existing name (looked up via the {{ name_option }} field. + required: True + type: str +{% endif %} +{% endfor %} + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str + version_added: "3.7" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +{% if examples[item_type] | default(False) %} +{{ examples[item_type] }} +{% endif %} +''' + +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( +{% for option in item['json']['actions']['POST'] %} +{% set option_data = [] %} +{{ option_data.append('required={}'.format(item['json']['actions']['POST'][option]['required'])) -}} +{{ option_data.append('type={}'.format(type_map[item['json']['actions']['POST'][option]['type']])) -}} +{% if item['json']['actions']['POST'][option]['type'] == 'password' %} +{{ option_data.append('no_log=True') -}} +{% endif %} +{% if 'choices' in item['json']['actions']['POST'][option] %} +{% set all_choices = [] %} +{% for choice in item['json']['actions']['POST'][option]['choices'] %} +{{ all_choices.append("'{}'".format(choice[0])) -}} +{% endfor %} +{{ option_data.append('choices=[{}]'.format(all_choices | join(', '))) -}} +{% endif %} +{% if 'default' in item['json']['actions']['POST'][option] %} +{{ option_data.append("default='{}'".format(item['json']['actions']['POST'][option]['default'])) -}} +{% endif %} +{% if aliases[item_type][option] | default(False) %} +{% set alias_list = [] %} +{% for alias_name in aliases[item_type][option] %} +{{ alias_list.append("'{}'".format(alias_name)) -}} +{% endfor %} +{{ option_data.append('aliases=[{}]'.format(alias_list | join(', '))) -}} +{% endif %} + {{ option }}=dict({{ option_data | join(', ') }}), +{% if option == name_option %} + new_{{ name_option }}=dict(required=False, type='str'), +{% endif %} +{% endfor %} + 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 +{% for option in item['json']['actions']['POST'] %} + {{ option }} = module.params.get('{{ option }}') +{% if 'min_value' in item['json']['actions']['POST'][option] %} + if {{ option }} < {{ item['json']['actions']['POST'][option]['min_value'] }}: + module.fail_msg(msg="The value for {{ option }} can not be less than {{ item['json']['actions']['POST'][option]['min_value'] }}") +{% endif %} +{% if 'max_value' in item['json']['actions']['POST'][option] %} + if {{ option }} > {{ item['json']['actions']['POST'][option]['max_value'] }}: + module.fail_msg(msg="The value for {{ option }} can not be larger than {{ item['json']['actions']['POST'][option]['max_value'] }}") +{% endif %} +{% if 'max_length' in item['json']['actions']['POST'][option] %} + if {{ option }} and len({{ option }}) > {{ item['json']['actions']['POST'][option]['max_length'] }}: + module.fail_msg(msg="The value for {{ option }} can not be longer than {{ item['json']['actions']['POST'][option]['max_length'] }}") +{% endif %} +{% if option == name_option %} + new_{{ name_option }} = module.params.get("new_{{ name_option }}") +{% endif %} +{% endfor %} + state = module.params.get('state') + +{% if item['json']['actions']['POST'] | length() > 0 %} + # Attempt to look up the related items the user specified (these will fail the module if not found) +{% for option in item['json']['actions']['POST'] %} +{% if item['json']['actions']['POST'][option]['type'] == 'id' %} + {{ option }}_id = None + if {{ option }}: + {{ option }}_id = module.resolve_name_to_id('{{ option }}', {{ option }}) +{% endif %} +{% endfor %} +{% endif %} + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('{{ item_type }}', **{ + 'data': { + '{{ name_option }}': {{ name_option }}, +{% if 'organization' in item['json']['actions']['POST'] and item['json']['actions']['POST']['organization']['type'] == 'id' %} + 'organization': org_id, +{% endif %} +{% if item_type in ['hosts', 'groups', 'inventory_sources'] %} + 'inventory': inventory_id, +{% endif %} + } + }) + + # Create the data that gets sent for create and update + new_fields = {} +{% for option in item['json']['actions']['POST'] %} +{% if option == name_option %} + new_fields['{{ name_option }}'] = new_{{ name_option }} if new_{{ name_option }} else {{ name_option }} +{% else %} + if {{ option }}: +{% if item['json']['actions']['POST'][option]['type'] == 'id' %} + new_fields['{{ option }}'] = {{ option }}_id +{% else %} + new_fields['{{ option }}'] = {{ option }} +{% endif %} +{% endif %} +{% endfor %} + + 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(existing_item) + elif state == 'present': + # 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='{{ item_type }}', item_type='{{ singular_item_type }}') + + +if __name__ == '__main__': + main() diff --git a/awx_collection/tools/vars/aliases.yml b/awx_collection/tools/vars/aliases.yml new file mode 100644 index 0000000000..03c7735c63 --- /dev/null +++ b/awx_collection/tools/vars/aliases.yml @@ -0,0 +1,29 @@ +--- +aliases: + job_templates: + ask_tags_on_launch: + - ask_tags + ask_verbosity_on_launch: + - ask_verbosity + ask_diff_mode_on_launch: + - ask_diff_mode + allow_simultaneous: + - concurrent_jobs_enabled + diff_mode: + - diff_mode_enabled + ask_inventory_on_launch: + - ask_inventory + limit: + - ask_limit + force_handlers: + - force_handlers_enabled + ask_job_type_on_launch: + - ask_job_type + ask_skip_tags_on_launch: + - ask_skip_tags + use_fact_cache: + - fact_caching_enabled + extra_vars: + - ask_extra_vars + ask_credential_on_launch: + - ask_credential diff --git a/awx_collection/tools/vars/examples.yml b/awx_collection/tools/vars/examples.yml new file mode 100644 index 0000000000..8a8d132881 --- /dev/null +++ b/awx_collection/tools/vars/examples.yml @@ -0,0 +1,52 @@ +--- +examples: + users: | + - name: Add tower user + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + first_name: John + last_name: Doe + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Add tower user as a system administrator + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + superuser: yes + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Add tower user as a system auditor + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + auditor: yes + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Delete tower user + tower_user: + username: jdoe + email: jdoe@example.org + state: absent + tower_config_file: "~/tower_cli.cfg" + + job_templates: | + - name: Create tower Ping job template + tower_job_template: + name: "Ping" + job_type: "run" + inventory: "Local" + project: "Demo" + playbook: "ping.yml" + credential: "Local" + state: "present" + tower_config_file: "~/tower_cli.cfg" + survey_enabled: yes + survey_spec: "{{ '{{' }} lookup('file', 'my_survey.json') {{ '}}' }}" + custom_virtualenv: "/var/lib/awx/venv/custom-venv/" diff --git a/awx_collection/tools/vars/generate_for.yml b/awx_collection/tools/vars/generate_for.yml new file mode 100644 index 0000000000..d44f4e002e --- /dev/null +++ b/awx_collection/tools/vars/generate_for.yml @@ -0,0 +1,15 @@ +--- +generate_for: +# - credential_types +# - groups +# - hosts +# - inventorues +# - inventory_sources +# - organizations +# - projects +# - teams + - users +# - job_templates +# - credentials +# - notification_templates +# - labels From 53d27c933ee69365e3537fa96ec2bdc3ac9c6659 Mon Sep 17 00:00:00 2001 From: beeankha Date: Wed, 11 Mar 2020 11:36:58 -0400 Subject: [PATCH 2/6] Fix linter issues --- awx_collection/tools/generate.yml | 6 ++--- awx_collection/tools/vars/generate_for.yml | 26 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/awx_collection/tools/generate.yml b/awx_collection/tools/generate.yml index 44772239cc..e5e98484f8 100644 --- a/awx_collection/tools/generate.yml +++ b/awx_collection/tools/generate.yml @@ -2,7 +2,7 @@ - name: Generate the awx.awx collection hosts: localhost connection: local - gather_facts: False + gather_facts: false vars: api_url: "{{ lookup('env', 'TOWER_HOST') }}" vars_files: @@ -11,8 +11,8 @@ - vars/examples.yml module_defaults: uri: - validate_certs: False - force_basic_auth: True + validate_certs: false + force_basic_auth: true url_username: "{{ lookup('env', 'TOWER_USERNAME') }}" url_password: "{{ lookup('env', 'TOWER_PASSWORD') }}" diff --git a/awx_collection/tools/vars/generate_for.yml b/awx_collection/tools/vars/generate_for.yml index d44f4e002e..3e6e578664 100644 --- a/awx_collection/tools/vars/generate_for.yml +++ b/awx_collection/tools/vars/generate_for.yml @@ -1,15 +1,15 @@ --- generate_for: -# - credential_types -# - groups -# - hosts -# - inventorues -# - inventory_sources -# - organizations -# - projects -# - teams - - users -# - job_templates -# - credentials -# - notification_templates -# - labels + # - credential_types + # - groups + # - hosts + # - inventorues + # - inventory_sources + # - organizations + # - projects + # - teams + # - users + # - job_templates + # - credentials + # - notification_templates + # - labels From 88c46b4573c1156d1e46a532627a5510139b1730 Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 12 Mar 2020 13:51:13 -0400 Subject: [PATCH 3/6] Add updated tower_api module util file, update generator and template --- .../plugins/module_utils/tower_api.py | 203 ++++++++++-------- .../tools/templates/tower_module.j2 | 1 + awx_collection/tools/vars/generate_for.yml | 9 +- 3 files changed, 114 insertions(+), 99 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index e395f53fe9..e84c29a363 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -111,7 +111,7 @@ class TowerModule(AnsibleModule): except ConfigFileException: self.fail_json('The config file {0} is not properly formatted'.format(config_file)) - # If we have a specified tower config, load it + # If we have a specified tower config, load it if self.params.get('tower_config_file'): duplicated_params = [] for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): @@ -126,7 +126,7 @@ class TowerModule(AnsibleModule): # TODO: warn if there are conflicts with other params self.load_config(self.params.get('tower_config_file')) except ConfigFileException as cfe: - # Since we were told specifically to load this we want it to fail if we have an error + # Since we were told specifically to load this, we want it to fail if we have an error self.fail_json(msg=cfe) def load_config(self, config_path): @@ -164,7 +164,7 @@ class TowerModule(AnsibleModule): else: config.readfp(placeholder_file) - # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + # If we made it to this point, then we have values from reading the ini file, so let's pull them out into a dict config_data = {} for honorred_setting in self.honorred_settings: try: @@ -262,7 +262,7 @@ class TowerModule(AnsibleModule): if response is not None: return name_or_id except ValueError: - # If we got a value error than we didn't have an integer so we can just pass and fall down to the fail + # If we got a value error, then we didn't have an integer so we can just pass and fall down to the fail pass self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) @@ -270,7 +270,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): - # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET + # In case someone is calling this directly; make sure we were given a method, let's not just assume a GET if not method: raise Exception("The HTTP method must be defined") @@ -290,7 +290,7 @@ class TowerModule(AnsibleModule): # This method will set a cookie in the cookie jar for us self.authenticate(**kwargs) if self.oauth_token: - # If we have a oauth token, we just use a bearer header + # If we have an oauth token, we just use a bearer header headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token) # Update the URL path with the endpoint @@ -379,7 +379,7 @@ class TowerModule(AnsibleModule): "application": None, "scope": "write", } - # Post to the tokens endpoint with baisc auth to try and get a token + # Post to the tokens endpoint with basic auth to try and get a token api_token_url = (self.url._replace(path='/api/v2/tokens/')).geturl() try: @@ -414,14 +414,14 @@ class TowerModule(AnsibleModule): except(Exception) as excinfo: self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) - 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, + def delete_if_needed(self, existing_item, on_delete=None): + # This will exit from the module on its own. + # If 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: + # This will return one of two things: # 1. None if the existing_item is not defined (so no delete needs to happen) # 2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module - # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False + # Note: common error codes from the Tower API can cause the module to fail if existing_item: # If we have an item, we can try to delete it try: @@ -440,9 +440,7 @@ class TowerModule(AnsibleModule): response = self.delete_endpoint(item_url) - if not handle_response: - return response - elif response['status_code'] in [202, 204]: + if response['status_code'] in [202, 204]: if on_delete: on_delete(self, response['json']) self.json_output['changed'] = True @@ -460,21 +458,40 @@ class TowerModule(AnsibleModule): else: self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) else: - if not handle_response: - return None - else: - self.exit_json(**self.json_output) + self.exit_json(**self.json_output) - 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, + def modify_associations(self, association_endpoint, new_association_list): + # First, get the existing associations + response = self.get_all_endpoint(association_endpoint) + existing_associated_ids = [association['id'] for association in response['json']['results']] + + # Disassociate anything that is in existing_associated_ids but not in new_association_list + ids_to_remove = list(set(existing_associated_ids) - set(new_association_list)) + for an_id in ids_to_remove: + response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) + if response['status_code'] == 204: + self.json_output['changed'] = True + else: + self.fail_json(msg="Failed to disassociate item {0}".format(response['json']['detail'])) + + # Associate anything that is in new_association_list but not in `association` + for an_id in list(set(new_association_list) - set(existing_associated_ids)): + response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) + if response['status_code'] == 204: + self.json_output['changed'] = True + else: + self.fail_json(msg="Failed to associate item {0}".format(response['json']['detail'])) + + def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, item_type='unknown', associations=None): + + # This will exit from the module on its own + # If 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: + # This will return one of two things: # 1. None if the existing_item is already defined (so no create needs to happen) # 2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module - # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False - # + # Note: common error codes from the Tower API can cause the module to fail + if not endpoint: self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type)) @@ -482,11 +499,7 @@ class TowerModule(AnsibleModule): try: 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: - return None - else: - self.exit_json(**self.json_output) + self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke)) else: # If we don't have an exisitng_item, we can try to create it @@ -495,9 +508,7 @@ class TowerModule(AnsibleModule): item_name = new_item.get('name', 'unknown') response = self.post_endpoint(endpoint, **{'data': new_item}) - if not handle_response: - return response - elif response['status_code'] == 201: + if response['status_code'] == 201: self.json_output['name'] = 'unknown' if 'name' in response['json']: self.json_output['name'] = response['json']['name'] @@ -506,10 +517,6 @@ class TowerModule(AnsibleModule): self.json_output['name'] = response['json']['username'] self.json_output['id'] = response['json']['id'] self.json_output['changed'] = True - if on_create is None: - self.exit_json(**self.json_output) - else: - on_create(self, response['json']) else: if 'json' in response and '__all__' in response['json']: self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0])) @@ -518,77 +525,83 @@ class TowerModule(AnsibleModule): else: 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, + # Process any associations with this item + if associations is not None: + for association_type in associations: + self.modify_associations(response, associations[association_type]) + + # If we have an on_create method and we actually changed something, we can call on_create + if on_create is not None and self.json_output['changed']: + on_create(self, response['json']) + else: + self.exit_json(**self.json_output) + + def update_if_needed(self, existing_item, new_item, on_update=None, associations=None): + # This will exit from the module on its own + # If 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: + # This will return one of three things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. # 3. An ItemNotDefined exception, if the existing_item does not exist - # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False - if existing_item: - # If we have an item, we can see if it needs an update - try: - item_url = existing_item['url'] - item_type = existing_item['type'] - if item_type == 'user': - item_name = existing_item['username'] - else: - item_name = existing_item['name'] - item_id = existing_item['id'] - except KeyError as ke: - self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke)) + # Note: common error codes from the Tower API can cause the module to fail + if not existing_item: + self.fail_json(msg="The exstiing item is not defined and thus cannot be updated") - needs_update = False - 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_field != new_field and not (existing_field in (None, '') and new_field == ''): - # Something doesn't match so let's update it - needs_update = True - break - - if needs_update: - response = self.patch_endpoint(item_url, **{'data': new_item}) - if not handle_response: - return response - elif response['status_code'] == 200: - self.json_output['changed'] = True - self.json_output['id'] = item_id - if on_update is None: - self.exit_json(**self.json_output) - else: - on_update(self, response['json']) - elif 'json' in response and '__all__' in response['json']: - self.fail_json(msg=response['json']['__all__']) - else: - self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response}) + # If we have an item, we can see if it needs an update + try: + item_url = existing_item['url'] + item_type = existing_item['type'] + if item_type == 'user': + item_name = existing_item['username'] else: - if not handle_response: - return None + item_name = existing_item['name'] + item_id = existing_item['id'] + except KeyError as ke: + self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke)) - # Since we made it here, we don't need to update, status ok - self.json_output['changed'] = False - self.json_output['id'] = item_id - self.exit_json(**self.json_output) - else: - if handle_response: - self.fail_json(msg="The exstiing item is not defined and thus cannot be updated") + # Check to see if anything within the item requires the item to be updated + needs_update = False + 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_field != new_field and not (existing_field in (None, '') and new_field == ''): + # Something doesn't match so let's update it + needs_update = True + break + + # If we decided the item needs to be updated, update it + self.json_output['id'] = item_id + if needs_update: + response = self.patch_endpoint(item_url, **{'data': new_item}) + if response['status_code'] == 200: + self.json_output['changed'] = True + elif 'json' in response and '__all__' in response['json']: + self.fail_json(msg=response['json']['__all__']) else: - raise ItemNotDefined("Not given an existing item to update") + self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response}) - def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, handle_response=True, item_type='unknown', on_create=None, on_update=None): - if existing_item: - return self.update_if_needed(existing_item, new_item, handle_response=handle_response, on_update=on_update) + # Process any associations with this item + if associations is not None: + for association_type in associations: + self.modify_associations(response['json']['related'][association_type], associations[association_type]) + + if on_update is not None and self.json_output['changed']: + on_update(self, response['json']) else: - return self.create_if_needed(existing_item, new_item, endpoint, handle_response=handle_response, on_create=on_create, item_type=item_type) + self.exit_json(**self.json_output) + + def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, associations=None): + if existing_item: + return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations) + else: + 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.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 + # Post to the tokens endpoint with basic auth to try and get a token api_token_url = (self.url._replace(path='/api/v2/tokens/{0}/'.format(self.oauth_token_id))).geturl() try: diff --git a/awx_collection/tools/templates/tower_module.j2 b/awx_collection/tools/templates/tower_module.j2 index 321901a574..bed690f9c5 100644 --- a/awx_collection/tools/templates/tower_module.j2 +++ b/awx_collection/tools/templates/tower_module.j2 @@ -87,6 +87,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( diff --git a/awx_collection/tools/vars/generate_for.yml b/awx_collection/tools/vars/generate_for.yml index 3e6e578664..029024d8ff 100644 --- a/awx_collection/tools/vars/generate_for.yml +++ b/awx_collection/tools/vars/generate_for.yml @@ -1,15 +1,16 @@ --- generate_for: + # - credentials # - credential_types # - groups # - hosts # - inventorues # - inventory_sources + # - job_templates + # - labels + # - notification_templates # - organizations # - projects + - schedules # - teams # - users - # - job_templates - # - credentials - # - notification_templates - # - labels From 97c169780d4915d5e3dd3f3594158a364a1d3835 Mon Sep 17 00:00:00 2001 From: beeankha Date: Fri, 13 Mar 2020 13:40:42 -0400 Subject: [PATCH 4/6] Delete config file --- awx_collection/tools/ansible.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 awx_collection/tools/ansible.cfg diff --git a/awx_collection/tools/ansible.cfg b/awx_collection/tools/ansible.cfg deleted file mode 100644 index 3efd4c3830..0000000000 --- a/awx_collection/tools/ansible.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[defaults] -stdout_callback = yaml From 2b5ff9a6f992a3c8b3e494735e6e38665c7e9a9b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 11 Mar 2020 11:39:27 -0400 Subject: [PATCH 5/6] Patches to generator to better align with modules --- awx_collection/plugins/modules/tower_user.py | 7 +++---- awx_collection/tools/generate.yml | 2 +- .../tools/templates/tower_module.j2 | 20 ++++--------------- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index ecddbc42c0..d052f8fa3a 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -45,7 +45,7 @@ options: type: str is_superuser: description: - - User is a system wide administrator. + - Designates that this user has all permissions without explicitly assigning them. required: False type: bool default: False @@ -59,10 +59,9 @@ options: aliases: ['auditor'] password: description: - - Password of the user; write-only field. + - Write-only field used to change the password. required: False type: str - default: '' state: description: - Desired state of the resource. @@ -128,7 +127,7 @@ def main(): email=dict(required=False, type='str'), is_superuser=dict(required=False, type='bool', default=False, aliases=['superuser']), is_system_auditor=dict(required=False, type='bool', default=False, aliases=['auditor']), - password=dict(required=False, type='str', default=''), + password=dict(required=False, type='str'), state=dict(choices=['present', 'absent'], default='present'), ) diff --git a/awx_collection/tools/generate.yml b/awx_collection/tools/generate.yml index e5e98484f8..20f86bbf85 100644 --- a/awx_collection/tools/generate.yml +++ b/awx_collection/tools/generate.yml @@ -57,7 +57,7 @@ - name: Process endpoint template: src: "templates/tower_module.j2" - dest: "modules/{{ file_name }}" + dest: "{{ playbook_dir | dirname }}/plugins/modules/{{ file_name }}" loop: "{{ end_point_options['results'] }}" loop_control: label: "{{ item['item']['key'] }}" diff --git a/awx_collection/tools/templates/tower_module.j2 b/awx_collection/tools/templates/tower_module.j2 index bed690f9c5..5592b14eb8 100644 --- a/awx_collection/tools/templates/tower_module.j2 +++ b/awx_collection/tools/templates/tower_module.j2 @@ -41,8 +41,8 @@ options: {% endif %} required: {{ item['json']['actions']['POST'][option]['required'] }} type: {{ type_map[ item['json']['actions']['POST'][option]['type'] ] }} -{% if 'default' in item['json']['actions']['POST'][option] %} - default: '{{ item['json']['actions']['POST'][option]['default'] }}' +{% if item['json']['actions']['POST'][option].get('default', '') != '' %} + default: {{ item['json']['actions']['POST'][option]['default'] }} {% endif %} {% if 'choices' in item['json']['actions']['POST'][option] %} choices: @@ -94,7 +94,7 @@ def main(): {% for option in item['json']['actions']['POST'] %} {% set option_data = [] %} {{ option_data.append('required={}'.format(item['json']['actions']['POST'][option]['required'])) -}} -{{ option_data.append('type={}'.format(type_map[item['json']['actions']['POST'][option]['type']])) -}} +{{ option_data.append('type=\'{}\''.format(type_map[item['json']['actions']['POST'][option]['type']])) -}} {% if item['json']['actions']['POST'][option]['type'] == 'password' %} {{ option_data.append('no_log=True') -}} {% endif %} @@ -105,7 +105,7 @@ def main(): {% endfor %} {{ option_data.append('choices=[{}]'.format(all_choices | join(', '))) -}} {% endif %} -{% if 'default' in item['json']['actions']['POST'][option] %} +{% if item['json']['actions']['POST'][option].get('default', '') != '' %} {{ option_data.append("default='{}'".format(item['json']['actions']['POST'][option]['default'])) -}} {% endif %} {% if aliases[item_type][option] | default(False) %} @@ -129,18 +129,6 @@ def main(): # Extract our parameters {% for option in item['json']['actions']['POST'] %} {{ option }} = module.params.get('{{ option }}') -{% if 'min_value' in item['json']['actions']['POST'][option] %} - if {{ option }} < {{ item['json']['actions']['POST'][option]['min_value'] }}: - module.fail_msg(msg="The value for {{ option }} can not be less than {{ item['json']['actions']['POST'][option]['min_value'] }}") -{% endif %} -{% if 'max_value' in item['json']['actions']['POST'][option] %} - if {{ option }} > {{ item['json']['actions']['POST'][option]['max_value'] }}: - module.fail_msg(msg="The value for {{ option }} can not be larger than {{ item['json']['actions']['POST'][option]['max_value'] }}") -{% endif %} -{% if 'max_length' in item['json']['actions']['POST'][option] %} - if {{ option }} and len({{ option }}) > {{ item['json']['actions']['POST'][option]['max_length'] }}: - module.fail_msg(msg="The value for {{ option }} can not be longer than {{ item['json']['actions']['POST'][option]['max_length'] }}") -{% endif %} {% if option == name_option %} new_{{ name_option }} = module.params.get("new_{{ name_option }}") {% endif %} From 6db6c6c5baa0a8b49acf702c4a2065e1962ddbc5 Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 16 Mar 2020 11:18:08 -0400 Subject: [PATCH 6/6] Revert module util changes, reorder params in group module --- .../plugins/module_utils/tower_api.py | 209 ++++++++---------- awx_collection/plugins/modules/tower_group.py | 12 +- 2 files changed, 104 insertions(+), 117 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index e84c29a363..e395f53fe9 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -111,7 +111,7 @@ class TowerModule(AnsibleModule): except ConfigFileException: self.fail_json('The config file {0} is not properly formatted'.format(config_file)) - # If we have a specified tower config, load it + # If we have a specified tower config, load it if self.params.get('tower_config_file'): duplicated_params = [] for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): @@ -126,7 +126,7 @@ class TowerModule(AnsibleModule): # TODO: warn if there are conflicts with other params self.load_config(self.params.get('tower_config_file')) except ConfigFileException as cfe: - # Since we were told specifically to load this, we want it to fail if we have an error + # Since we were told specifically to load this we want it to fail if we have an error self.fail_json(msg=cfe) def load_config(self, config_path): @@ -164,7 +164,7 @@ class TowerModule(AnsibleModule): else: config.readfp(placeholder_file) - # If we made it to this point, then we have values from reading the ini file, so let's pull them out into a dict + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict config_data = {} for honorred_setting in self.honorred_settings: try: @@ -262,7 +262,7 @@ class TowerModule(AnsibleModule): if response is not None: return name_or_id except ValueError: - # If we got a value error, then we didn't have an integer so we can just pass and fall down to the fail + # If we got a value error than we didn't have an integer so we can just pass and fall down to the fail pass self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) @@ -270,7 +270,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): - # In case someone is calling this directly; make sure we were given a method, let's not just assume a GET + # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET if not method: raise Exception("The HTTP method must be defined") @@ -290,7 +290,7 @@ class TowerModule(AnsibleModule): # This method will set a cookie in the cookie jar for us self.authenticate(**kwargs) if self.oauth_token: - # If we have an oauth token, we just use a bearer header + # If we have a oauth token, we just use a bearer header headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token) # Update the URL path with the endpoint @@ -379,7 +379,7 @@ class TowerModule(AnsibleModule): "application": None, "scope": "write", } - # Post to the tokens endpoint with basic auth to try and get a token + # Post to the tokens endpoint with baisc auth to try and get a token api_token_url = (self.url._replace(path='/api/v2/tokens/')).geturl() try: @@ -414,14 +414,14 @@ class TowerModule(AnsibleModule): except(Exception) as excinfo: self.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) - def delete_if_needed(self, existing_item, on_delete=None): - # This will exit from the module on its own. - # If the method successfully deletes an item and on_delete param is defined, + 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, # the on_delete parameter will be called as a method pasing in this object and the json from the response - # This will return one of two things: + # If you pass handle_response=False, it will return one of two things: # 1. None if the existing_item is not defined (so no delete needs to happen) # 2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module - # Note: common error codes from the Tower API can cause the module to fail + # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False if existing_item: # If we have an item, we can try to delete it try: @@ -440,7 +440,9 @@ class TowerModule(AnsibleModule): response = self.delete_endpoint(item_url) - if response['status_code'] in [202, 204]: + if not handle_response: + return response + elif response['status_code'] in [202, 204]: if on_delete: on_delete(self, response['json']) self.json_output['changed'] = True @@ -458,40 +460,21 @@ class TowerModule(AnsibleModule): else: self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) else: - self.exit_json(**self.json_output) - - def modify_associations(self, association_endpoint, new_association_list): - # First, get the existing associations - response = self.get_all_endpoint(association_endpoint) - existing_associated_ids = [association['id'] for association in response['json']['results']] - - # Disassociate anything that is in existing_associated_ids but not in new_association_list - ids_to_remove = list(set(existing_associated_ids) - set(new_association_list)) - for an_id in ids_to_remove: - response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) - if response['status_code'] == 204: - self.json_output['changed'] = True + if not handle_response: + return None else: - self.fail_json(msg="Failed to disassociate item {0}".format(response['json']['detail'])) + self.exit_json(**self.json_output) - # Associate anything that is in new_association_list but not in `association` - for an_id in list(set(new_association_list) - set(existing_associated_ids)): - response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) - if response['status_code'] == 204: - self.json_output['changed'] = True - else: - self.fail_json(msg="Failed to associate item {0}".format(response['json']['detail'])) - - def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, item_type='unknown', associations=None): - - # This will exit from the module on its own - # If the method successfully creates an item and on_create param is defined, + 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, # the on_create parameter will be called as a method pasing in this object and the json from the response - # This will return one of two things: + # If you pass handle_response=False it will return one of two things: # 1. None if the existing_item is already defined (so no create needs to happen) # 2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module - # Note: common error codes from the Tower API can cause the module to fail - + # 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 {0} due to missing endpoint".format(item_type)) @@ -499,7 +482,11 @@ class TowerModule(AnsibleModule): try: existing_item['url'] except KeyError as ke: - self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke)) + self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke)) + if not handle_response: + return None + else: + self.exit_json(**self.json_output) else: # If we don't have an exisitng_item, we can try to create it @@ -508,7 +495,9 @@ class TowerModule(AnsibleModule): item_name = new_item.get('name', 'unknown') response = self.post_endpoint(endpoint, **{'data': new_item}) - if response['status_code'] == 201: + if not handle_response: + return response + elif response['status_code'] == 201: self.json_output['name'] = 'unknown' if 'name' in response['json']: self.json_output['name'] = response['json']['name'] @@ -517,6 +506,10 @@ class TowerModule(AnsibleModule): self.json_output['name'] = response['json']['username'] self.json_output['id'] = response['json']['id'] self.json_output['changed'] = True + if on_create is None: + self.exit_json(**self.json_output) + else: + on_create(self, response['json']) else: if 'json' in response and '__all__' in response['json']: self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0])) @@ -525,83 +518,77 @@ class TowerModule(AnsibleModule): else: self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code'])) - # Process any associations with this item - if associations is not None: - for association_type in associations: - self.modify_associations(response, associations[association_type]) - - # If we have an on_create method and we actually changed something, we can call on_create - if on_create is not None and self.json_output['changed']: - on_create(self, response['json']) - else: - self.exit_json(**self.json_output) - - def update_if_needed(self, existing_item, new_item, on_update=None, associations=None): - # This will exit from the module on its own - # If the method successfully updates an item and on_update param is defined, + 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, # the on_update parameter will be called as a method pasing in this object and the json from the response - # This will return one of three things: + # If you pass handle_response=False it will return one of three things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. # 3. An ItemNotDefined exception, if the existing_item does not exist - # Note: common error codes from the Tower API can cause the module to fail - if not existing_item: - self.fail_json(msg="The exstiing item is not defined and thus cannot be updated") - - # If we have an item, we can see if it needs an update - try: - item_url = existing_item['url'] - item_type = existing_item['type'] - if item_type == 'user': - item_name = existing_item['username'] - else: - item_name = existing_item['name'] - item_id = existing_item['id'] - except KeyError as ke: - self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke)) - - # Check to see if anything within the item requires the item to be updated - needs_update = False - 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_field != new_field and not (existing_field in (None, '') and new_field == ''): - # Something doesn't match so let's update it - needs_update = True - break - - # If we decided the item needs to be updated, update it - self.json_output['id'] = item_id - if needs_update: - response = self.patch_endpoint(item_url, **{'data': new_item}) - if response['status_code'] == 200: - self.json_output['changed'] = True - elif 'json' in response and '__all__' in response['json']: - self.fail_json(msg=response['json']['__all__']) - else: - self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response}) - - # Process any associations with this item - if associations is not None: - for association_type in associations: - self.modify_associations(response['json']['related'][association_type], associations[association_type]) - - if on_update is not None and self.json_output['changed']: - on_update(self, response['json']) - else: - self.exit_json(**self.json_output) - - def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, associations=None): + # Note: common error codes from the Tower API can cause the module to fail even if handle_response is set to False if existing_item: - return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations) + # If we have an item, we can see if it needs an update + try: + item_url = existing_item['url'] + item_type = existing_item['type'] + if item_type == 'user': + item_name = existing_item['username'] + else: + item_name = existing_item['name'] + item_id = existing_item['id'] + except KeyError as ke: + self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke)) + + needs_update = False + 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_field != new_field and not (existing_field in (None, '') and new_field == ''): + # Something doesn't match so let's update it + needs_update = True + break + + if needs_update: + response = self.patch_endpoint(item_url, **{'data': new_item}) + if not handle_response: + return response + elif response['status_code'] == 200: + self.json_output['changed'] = True + self.json_output['id'] = item_id + if on_update is None: + self.exit_json(**self.json_output) + else: + on_update(self, response['json']) + elif 'json' in response and '__all__' in response['json']: + self.fail_json(msg=response['json']['__all__']) + else: + self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response}) + else: + if not handle_response: + return None + + # Since we made it here, we don't need to update, status ok + self.json_output['changed'] = False + self.json_output['id'] = item_id + self.exit_json(**self.json_output) else: - return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) + if handle_response: + self.fail_json(msg="The exstiing item is not defined and thus cannot be updated") + else: + raise ItemNotDefined("Not given an existing item to update") + + def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, handle_response=True, item_type='unknown', on_create=None, on_update=None): + if existing_item: + return self.update_if_needed(existing_item, new_item, handle_response=handle_response, on_update=on_update) + else: + return self.create_if_needed(existing_item, new_item, endpoint, handle_response=handle_response, on_create=on_create, item_type=item_type) def logout(self): 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 basic auth to try and get a token + # 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() try: diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index bb0b22ae77..4b68a50bb0 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -28,12 +28,6 @@ options: - The name to use for the group. required: True type: str - new_name: - description: - - A new name for this group (for renaming) - required: False - type: str - version_added: "3.7" description: description: - The description to use for the group. @@ -53,6 +47,12 @@ options: default: "present" choices: ["present", "absent"] type: str + new_name: + description: + - A new name for this group (for renaming) + required: False + type: str + version_added: "3.7" tower_oauthtoken: description: - The Tower OAuth token to use.