diff --git a/ci/roles/keystone_mapping/defaults/main.yml b/ci/roles/keystone_mapping/defaults/main.yml new file mode 100644 index 00000000..6cc843eb --- /dev/null +++ b/ci/roles/keystone_mapping/defaults/main.yml @@ -0,0 +1,22 @@ +mapping_name: 'ansible-test-mapping' +mapping_rules_1: +- local: + - group: + domain: + name: example_domain + name: example-group + remote: + - type: HTTP_OIDC_GROUPS + any_one_of: + - group1 + - group2 +mapping_rules_2: +- local: + - group: + domain: + name: example_domain + name: example_group + remote: + - type: HTTP_OIDC_GROUPS + any_one_of: + - group1 diff --git a/ci/roles/keystone_mapping/tasks/main.yml b/ci/roles/keystone_mapping/tasks/main.yml new file mode 100644 index 00000000..93cdea79 --- /dev/null +++ b/ci/roles/keystone_mapping/tasks/main.yml @@ -0,0 +1,169 @@ +--- +- module_defaults: + # meta/action_groups.yml glue seems to be missing + # group/os: + # cloud: "{{ cloud }}" + openstack.cloud.os_keystone_mapping: + cloud: "{{ cloud }}" + block: + - name: "Ensure mapping doesn't exist to start" + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + register: delete_mapping + - assert: + that: + - delete_mapping is successful + + - name: 'Create mapping - CHECK_MODE' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_1 }}' + register: create_mapping + check_mode: yes + - assert: + that: + - create_mapping is successful + - create_mapping is changed + + - name: 'Create mapping' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_1 }}' + register: create_mapping + - assert: + that: + - create_mapping is successful + - create_mapping is changed + - '"id" in create_mapping.mapping' + - '"name" in create_mapping.mapping' + - '"rules" in create_mapping.mapping' + - create_mapping.mapping.id == mapping_name + - create_mapping.mapping.name == mapping_name + - create_mapping.mapping.rules | length == 1 + + - name: 'Create mapping (retry - no change) - CHECK_MODE' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_1 }}' + register: create_mapping + check_mode: yes + - assert: + that: + - create_mapping is successful + - create_mapping is not changed + + - name: 'Create mapping (retry - no change)' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_1 }}' + register: create_mapping + - assert: + that: + - create_mapping is successful + - create_mapping is not changed + - '"id" in create_mapping.mapping' + - '"name" in create_mapping.mapping' + - '"rules" in create_mapping.mapping' + - create_mapping.mapping.id == mapping_name + - create_mapping.mapping.name == mapping_name + - create_mapping.mapping.rules | length == 1 + + - name: 'Update mapping - CHECK_MODE' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_2 }}' + register: update_mapping + check_mode: yes + - assert: + that: + - update_mapping is successful + - update_mapping is changed + + - name: 'Update mapping' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_2 }}' + register: update_mapping + - assert: + that: + - update_mapping is successful + - update_mapping is changed + - '"id" in update_mapping.mapping' + - '"name" in update_mapping.mapping' + - '"rules" in update_mapping.mapping' + - update_mapping.mapping.id == mapping_name + - update_mapping.mapping.name == mapping_name + - update_mapping.mapping.rules | length == 1 + + - name: 'Update mapping (retry - no change)' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name }}' + rules: '{{ mapping_rules_2 }}' + register: update_mapping + - assert: + that: + - update_mapping is successful + - update_mapping is not changed + - '"id" in update_mapping.mapping' + - '"name" in update_mapping.mapping' + - '"rules" in update_mapping.mapping' + - update_mapping.mapping.id == mapping_name + - update_mapping.mapping.name == mapping_name + - update_mapping.mapping.rules | length == 1 + + - name: 'Delete mapping - CHECK_MODE' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + register: delete_mapping + check_mode: yes + - assert: + that: + - delete_mapping is successful + - delete_mapping is changed + + - name: 'Delete mapping' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + register: delete_mapping + - assert: + that: + - delete_mapping is successful + - delete_mapping is changed + + - name: 'Delete mapping (retry - no change) - CHECK_MODE' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + register: delete_mapping + check_mode: yes + - assert: + that: + - delete_mapping is successful + - delete_mapping is not changed + + - name: 'Delete mapping (retry - no change) ' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + register: delete_mapping + - assert: + that: + - delete_mapping is successful + - delete_mapping is not changed + + always: + - name: 'Delete mapping' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name }}' + ignore_errors: yes diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 78e10412..4014af64 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -12,6 +12,7 @@ # - { role: image, tags: image } - { role: keypair, tags: keypair } - { role: keystone_domain, tags: keystone_domain } + - { role: keystone_mapping, tags: keystone_mapping } - { role: keystone_role, tags: keystone_role } - { role: network, tags: network } - { role: nova_flavor, tags: nova_flavor } diff --git a/meta/action_groups.yml b/meta/action_groups.yml index c00f966d..a3e7d287 100644 --- a/meta/action_groups.yml +++ b/meta/action_groups.yml @@ -16,6 +16,7 @@ os: - os_keystone_domain - os_keystone_domain_info - os_keystone_endpoint +- os_keystone_mapping - os_keystone_role - os_keystone_service - os_listener diff --git a/plugins/modules/os_keystone_mapping.py b/plugins/modules/os_keystone_mapping.py new file mode 100644 index 00000000..5cdb40a3 --- /dev/null +++ b/plugins/modules/os_keystone_mapping.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: os_keystone_mapping +short_description: Manage a federation mapping +author: + - "Mark Chappell " +description: + - Manage a federation mapping. +options: + name: + description: + - The name of the mapping to manage. + required: true + type: str + aliases: ['id'] + state: + description: + - Whether the mapping should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + rules: + description: + - The rules that comprise the mapping. These are pairs of I(local) and + I(remote) definitions. For more details on how these work please see + the OpenStack documentation + U(https://docs.openstack.org/keystone/latest/admin/federation/mapping_combinations.html). + - Required if I(state=present) + type: list + elements: dict + suboptions: + local: + description: + - Information on what local attributes will be mapped. + required: true + type: list + elements: dict + remote: + description: + - Information on what remote attributes will be mapped. + required: true + type: list + elements: dict +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a new mapping + os_keystone_mapping: + cloud: example_cloud + name: example_mapping + rules: + - local: + - user: + name: '{0}' + - group: + id: '0cd5e9' + remote: + - type: UserName + - type: orgPersonType + any_one_of: + - Contractor + - SubContractor + +- name: Delete a mapping + os_keystone_mapping: + name: example_mapping + state: absent +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + openstack_full_argument_spec, + openstack_module_kwargs, + openstack_cloud_from_module) + + +def normalize_mapping(mapping): + """ + Normalizes the mapping definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if mapping is None: + return None + + _mapping = dict(mapping) + _mapping['name'] = mapping['id'] + return _mapping + + +def create_mapping(module, sdk, cloud, name): + """ + Attempt to create a Mapping + + returns: A tuple containing the "Changed" state and the created mapping + """ + + if module.check_mode: + return (True, None) + + rules = module.params.get('rules') + + try: + mapping = cloud.identity.create_mapping(id=name, rules=rules) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to create mapping: {0}'.format(str(ex))) + return (True, mapping) + + +def delete_mapping(module, sdk, cloud, mapping): + """ + Attempt to delete a Mapping + + returns: the "Changed" state + """ + if mapping is None: + return False + + if module.check_mode: + return True + + try: + cloud.identity.delete_mapping(mapping) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to delete mapping: {0}'.format(str(ex))) + return True + + +def update_mapping(module, sdk, cloud, mapping): + """ + Attempt to delete a Mapping + + returns: The "Changed" state and the the new mapping + """ + + current_rules = mapping.rules + new_rules = module.params.get('rules') + + # Nothing to do + if current_rules == new_rules: + return (False, mapping) + + if module.check_mode: + return (True, None) + + try: + new_mapping = cloud.identity.update_mapping(mapping, rules=new_rules) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to update mapping: {0}'.format(str(ex))) + return (True, new_mapping) + + +def main(): + """ Module entry point """ + + argument_spec = openstack_full_argument_spec( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + rules=dict(type='list', elements='dict', options=dict( + local=dict(required=True, type='list', elements='dict'), + remote=dict(required=True, type='list', elements='dict') + )), + ) + module_kwargs = openstack_module_kwargs( + required_if=[('state', 'present', ['rules'])] + ) + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + **module_kwargs + ) + + name = module.params.get('name') + state = module.params.get('state') + changed = False + + sdk, cloud = openstack_cloud_from_module(module, min_version="0.44") + + try: + mapping = cloud.identity.get_mapping(name) + except sdk.exceptions.ResourceNotFound: + mapping = None + + if state == 'absent': + if mapping is not None: + changed = delete_mapping(module, sdk, cloud, mapping) + module.exit_json(changed=changed) + + if len(module.params.get('rules')) < 1: + module.fail_json(msg='At least one rule must be passed') + + if mapping is None: + (changed, mapping) = create_mapping(module, sdk, cloud, name) + mapping = normalize_mapping(mapping) + module.exit_json(changed=changed, mapping=mapping) + + (changed, new_mapping) = update_mapping(module, sdk, cloud, mapping) + new_mapping = normalize_mapping(new_mapping) + module.exit_json(mapping=new_mapping, changed=changed) + + +if __name__ == '__main__': + main()