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