From 8729b8da72ec901382e7b6abf1453df7aabf1891 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Wed, 25 Mar 2020 17:31:02 +0100 Subject: [PATCH] New Module: os_keystone_federation_protocol Add support for Keystone federation Protocols Depends-On: https://review.opendev.org/714431 Depends-On: https://review.opendev.org/713461 Change-Id: I6dff6cebe72106e601834976e369e08583391c55 --- .../defaults/main.yml | 35 ++ .../tasks/main.yml | 393 ++++++++++++++++++ ci/run-collection.yml | 3 + meta/action_groups.yml | 1 + .../os_keystone_federation_protocol.py | 217 ++++++++++ 5 files changed, 649 insertions(+) create mode 100644 ci/roles/keystone_federation_protocol/defaults/main.yml create mode 100644 ci/roles/keystone_federation_protocol/tasks/main.yml create mode 100644 plugins/modules/os_keystone_federation_protocol.py diff --git a/ci/roles/keystone_federation_protocol/defaults/main.yml b/ci/roles/keystone_federation_protocol/defaults/main.yml new file mode 100644 index 00000000..d6653385 --- /dev/null +++ b/ci/roles/keystone_federation_protocol/defaults/main.yml @@ -0,0 +1,35 @@ +protocol_name: 'test-protocol' +protocol_name_2: 'test-protocol-2' + +# Minimal IDP definition +idp_name: 'test-idp' +idp_remote_ids: +- 'https://auth.example.com/auth/realms/ExampleRealm' + +# Minimal Domain definition +domain_name: 'test-domain' + +# Minimal Mapping definition +mapping_name_1: 'ansible-test-mapping-1' +mapping_name_2: 'ansible-test-mapping-2' +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_federation_protocol/tasks/main.yml b/ci/roles/keystone_federation_protocol/tasks/main.yml new file mode 100644 index 00000000..ce5e81b1 --- /dev/null +++ b/ci/roles/keystone_federation_protocol/tasks/main.yml @@ -0,0 +1,393 @@ +--- +# General run of tests +# - Make change - Check mode +# - Make change +# - Retry change (noop) - Check mode +# - Retry change (noop) +# +- module_defaults: + # meta/action_groups.yml glue seems to be missing + # group/os: + # cloud: "{{ cloud }}" + openstack.cloud.os_keystone_domain: + cloud: "{{ cloud }}" + openstack.cloud.os_keystone_identity_provider: + cloud: "{{ cloud }}" + openstack.cloud.os_keystone_mapping: + cloud: "{{ cloud }}" + openstack.cloud.os_keystone_federation_protocol: + cloud: "{{ cloud }}" + idp_id: "{{ idp_name }}" + #openstack.cloud.os_keystone_federation_protocol_info: + # cloud: "{{ cloud }}" + # idp_id: "{{ idp_name }}" + block: + # ======================================================================== + # Initial setup + - name: 'Create test Domain' + openstack.cloud.os_keystone_domain: + name: '{{ domain_name }}' + register: create_domain + - assert: + that: + - create_domain is successful + - '"id" in create_domain' + - name: 'Store domain ID as fact' + set_fact: + domain_id: '{{ create_domain.id }}' + + - name: 'Create test Identity Provider' + openstack.cloud.os_keystone_identity_provider: + state: 'present' + name: '{{ idp_name }}' + domain_id: '{{ domain_id }}' + register: create_idp + - assert: + that: + - create_idp is successful + + - name: 'Create test mapping (1)' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name_1 }}' + rules: '{{ mapping_rules_1 }}' + register: create_mapping + - assert: + that: + - create_mapping is successful + - name: 'Create test mapping (2)' + openstack.cloud.os_keystone_mapping: + state: 'present' + name: '{{ mapping_name_2 }}' + rules: '{{ mapping_rules_2 }}' + register: create_mapping + - assert: + that: + - create_mapping is successful + + # We *should* have a blank slate to start with, but we also shouldn't + # explode if I(state=absent) and the IDP doesn't exist + - name: "Ensure Protocol doesn't exist to start" + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + register: delete_protocol + - assert: + that: + - delete_protocol is successful + + # ======================================================================== + # Creation + + - name: 'Create protocol - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_1 }}' + register: create_protocol + - assert: + that: + - create_protocol is successful + - create_protocol is changed + + #- name: 'Fetch Protocol info (should be absent)' + # openstack.cloud.os_keystone_federation_protocol_info: + # name: '{{ protocol_name }}' + # register: protocol_info + # ignore_errors: yes + #- assert: + # that: + # - protocol_info is failed + + - name: 'Create protocol' + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_1 }}' + register: create_protocol + - assert: + that: + - create_protocol is successful + - create_protocol is changed + - '"protocol" in create_protocol' + - '"id" in protocol' + - '"name" in protocol' + - '"idp_id" in protocol' + - '"mapping_id" in protocol' + - protocol.id == protocol_name + - protocol.name == protocol_name + - protocol.idp_id == idp_name + - protocol.mapping_id == mapping_name_1 + vars: + protocol: '{{ create_protocol.protocol }}' + + - name: 'Create protocol (retry - no change) - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_1 }}' + register: create_protocol + - assert: + that: + - create_protocol is successful + - create_protocol is not changed + + - name: 'Create protocol (retry - no change)' + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_1 }}' + register: create_protocol + - assert: + that: + - create_protocol is successful + - create_protocol is not changed + - '"protocol" in create_protocol' + - '"id" in protocol' + - '"name" in protocol' + - '"idp_id" in protocol' + - '"mapping_id" in protocol' + - protocol.id == protocol_name + - protocol.name == protocol_name + - protocol.idp_id == idp_name + - protocol.mapping_id == mapping_name_1 + vars: + protocol: '{{ create_protocol.protocol }}' + + # ======================================================================== + # Update + + - name: 'Update protocol - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_2 }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is changed + + - name: 'Update protocol' + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_2 }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is changed + - '"protocol" in update_protocol' + - '"id" in protocol' + - '"name" in protocol' + - '"idp_id" in protocol' + - '"mapping_id" in protocol' + - protocol.id == protocol_name + - protocol.name == protocol_name + - protocol.idp_id == idp_name + - protocol.mapping_id == mapping_name_2 + vars: + protocol: '{{ update_protocol.protocol }}' + + - name: 'Update protocol (retry - no change) - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_2 }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is not changed + + - name: 'Update protocol (retry - no change)' + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name }}' + mapping_id: '{{ mapping_name_2 }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is not changed + - '"protocol" in update_protocol' + - '"id" in protocol' + - '"name" in protocol' + - '"idp_id" in protocol' + - '"mapping_id" in protocol' + - protocol.id == protocol_name + - protocol.name == protocol_name + - protocol.idp_id == idp_name + - protocol.mapping_id == mapping_name_2 + vars: + protocol: '{{ update_protocol.protocol }}' + + # ======================================================================== + # Create second protocol to test os_keystone_federation_protocol_info + + - name: 'Create protocol (2)' + openstack.cloud.os_keystone_federation_protocol: + state: 'present' + name: '{{ protocol_name_2 }}' + mapping_id: '{{ mapping_name_1 }}' + register: create_protocol_2 + - assert: + that: + - create_protocol_2 is successful + - create_protocol_2 is changed + - '"protocol" in create_protocol_2' + - '"id" in protocol' + - '"name" in protocol' + - '"idp_id" in protocol' + - '"mapping_id" in protocol' + - protocol.id == protocol_name_2 + - protocol.name == protocol_name_2 + - protocol.idp_id == idp_name + - protocol.mapping_id == mapping_name_1 + vars: + protocol: '{{ create_protocol_2.protocol }}' + + # ======================================================================== + # Basic tests of os_keystone_federation_protocol_info + + #- name: 'Fetch Protocol info (a specific protocol)' + # openstack.cloud.os_keystone_federation_protocol_info: + # name: '{{ protocol_name }}' + # register: protocol_info + #- assert: + # that: + # - protocol_info is successful + # - '"protocols" in protocol_info' + # - protocol_info.protocols | length == 1 + # - '"id" in protocol' + # - '"name" in protocol' + # - '"idp_id" in protocol' + # - '"mapping_id" in protocol' + # - protocol.id == protocol_name + # - protocol.name == protocol_name + # - protocol.idp_id == idp_name + # - protocol.mapping_id == mapping_name_2 + # vars: + # protocol: '{{ protocol_info.protocols[0] }}' + + #- name: 'Fetch Protocol info (all protocols on our test IDP)' + # openstack.cloud.os_keystone_federation_protocol_info: {} + # # idp_id defined in defaults at the start + # register: protocol_info + #- assert: + # that: + # - protocol_info is successful + # - '"protocols" in protocol_info' + # # We created the IDP, and we're going to delete it: + # # we should be able to trust what's attached to it + # - protocol_info.protocols | length == 2 + # - '"id" in protocol_1' + # - '"name" in protocol_1' + # - '"idp_id" in protocol_1' + # - '"mapping_id" in protocol_1' + # - '"id" in protocol_2' + # - '"name" in protocol_2' + # - '"idp_id" in protocol_2' + # - '"mapping_id" in protocol_2' + # - protocol_name in (protocol_info.protocols | map(attribute='id')) + # - protocol_name in (protocol_info.protocols | map(attribute='id')) + # - protocol_name_2 in (protocol_info.protocols | map(attribute='name')) + # - protocol_name_2 in (protocol_info.protocols | map(attribute='name')) + # - mapping_name_1 in (protocol_info.protocols | map(attribute='mapping_id')) + # - mapping_name_2 in (protocol_info.protocols | map(attribute='mapping_id')) + # - protocol_1.idp_id == idp_name + # - protocol_2.idp_id == idp_name + # vars: + # protocol_1: '{{ protocol_info.protocols[0] }}' + # protocol_2: '{{ protocol_info.protocols[1] }}' + + # ======================================================================== + # Deletion + + - name: 'Delete protocol - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is changed + + - name: 'Delete protocol' + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is changed + + - name: 'Delete protocol (retry - no change) - CHECK MODE' + check_mode: yes + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is not changed + + - name: 'Delete protocol (retry - no change)' + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + register: update_protocol + - assert: + that: + - update_protocol is successful + - update_protocol is not changed + + # ======================================================================== + # Clean up after ourselves + always: + - name: 'Delete protocol' + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name }}' + idp_id: '{{ idp_name }}' + ignore_errors: yes + + - name: 'Delete protocol (2)' + openstack.cloud.os_keystone_federation_protocol: + state: 'absent' + name: '{{ protocol_name_2 }}' + idp_id: '{{ idp_name }}' + ignore_errors: yes + + - name: 'Delete mapping 1' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name_1 }}' + ignore_errors: yes + + - name: 'Delete mapping 2' + openstack.cloud.os_keystone_mapping: + state: 'absent' + name: '{{ mapping_name_2 }}' + ignore_errors: yes + + - name: 'Delete idp' + openstack.cloud.os_keystone_identity_provider: + state: 'absent' + name: '{{ idp_name }}' + ignore_errors: yes + + - name: 'Delete domain' + openstack.cloud.os_keystone_domain: + state: 'absent' + name: '{{ domain_name }}' + ignore_errors: yes diff --git a/ci/run-collection.yml b/ci/run-collection.yml index fabca7be..19dee161 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -18,6 +18,9 @@ - role: keystone_idp tags: keystone_idp when: sdk_version is version(0.44, '>=') + - role: keystone_federation_protocol + tags: keystone_federation_protocol + when: sdk_version is version(0.44, '>=') - { 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 b2fa5671..7bb63aaf 100644 --- a/meta/action_groups.yml +++ b/meta/action_groups.yml @@ -20,6 +20,7 @@ os: - os_keystone_identity_provider_info - os_keystone_mapping - os_keystone_mapping_info +- os_keystone_federation_protocol - os_keystone_role - os_keystone_service - os_listener diff --git a/plugins/modules/os_keystone_federation_protocol.py b/plugins/modules/os_keystone_federation_protocol.py new file mode 100644 index 00000000..b77c0e85 --- /dev/null +++ b/plugins/modules/os_keystone_federation_protocol.py @@ -0,0 +1,217 @@ +#!/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_federation_protocol +short_description: manage a federation Protocol +author: + - "Mark Chappell (@tremble) " +description: + - Manage a federation Protocol. +options: + name: + description: + - The name of the Protocol. + type: str + required: true + aliases: ['id'] + state: + description: + - Whether the protocol should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str + idp_id: + description: + - The name of the Identity Provider this Protocol is associated with. + aliases: ['idp_name'] + required: true + type: str + mapping_id: + description: + - The name of the Mapping to use for this Protocol.' + - Required when creating a new Protocol. + type: str + aliases: ['mapping_name'] +requirements: + - "python >= 3.6" + - "openstacksdk >= 0.44" +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = ''' +- name: Create a protocol + os_keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + mapping_id: example_mapping + +- name: Delete a protocol + os_keystone_federation_protocol: + cloud: example_cloud + name: example_protocol + idp_id: example_idp + state: absent +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_full_argument_spec +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_module_kwargs +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_cloud_from_module + + +def normalize_protocol(protocol): + """ + Normalizes the protocol definitions so that the outputs are consistent with the + parameters + + - "name" (parameter) == "id" (SDK) + """ + if protocol is None: + return None + + _protocol = protocol.to_dict() + _protocol['name'] = protocol['id'] + # As of 0.44 SDK doesn't copy the URI parameters over, so let's add them + _protocol['idp_id'] = protocol['idp_id'] + return _protocol + + +def delete_protocol(module, sdk, cloud, protocol): + """ + Delete an existing Protocol + + returns: the "Changed" state + """ + + if protocol is None: + return False + + if module.check_mode: + return True + + try: + cloud.identity.delete_federation_protocol(None, protocol) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to delete protocol: {0}'.format(str(ex))) + return True + + +def create_protocol(module, sdk, cloud, name): + """ + Create a new Protocol + + returns: the "Changed" state and the new protocol + """ + + if module.check_mode: + return True, None + + idp_name = module.params.get('idp_id') + mapping_id = module.params.get('mapping_id') + + attributes = { + 'idp_id': idp_name, + 'mapping_id': mapping_id, + } + + try: + protocol = cloud.identity.create_federation_protocol(id=name, **attributes) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to create protocol: {0}'.format(str(ex))) + return (True, protocol) + + +def update_protocol(module, sdk, cloud, protocol): + """ + Update an existing Protocol + + returns: the "Changed" state and the new protocol + """ + + mapping_id = module.params.get('mapping_id') + + attributes = {} + + if (mapping_id is not None) and (mapping_id != protocol.mapping_id): + attributes['mapping_id'] = mapping_id + + if not attributes: + return False, protocol + + if module.check_mode: + return True, None + + try: + new_protocol = cloud.identity.update_federation_protocol(None, protocol, **attributes) + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to update protocol: {0}'.format(str(ex))) + return (True, new_protocol) + + +def main(): + """ Module entry point """ + + argument_spec = openstack_full_argument_spec( + name=dict(required=True, aliases=['id']), + state=dict(default='present', choices=['absent', 'present']), + idp_id=dict(required=True, aliases=['idp_name']), + mapping_id=dict(aliases=['mapping_name']), + ) + module_kwargs = openstack_module_kwargs( + ) + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + **module_kwargs + ) + + name = module.params.get('name') + state = module.params.get('state') + idp = module.params.get('idp_id') + changed = False + + sdk, cloud = openstack_cloud_from_module(module, min_version="0.44") + + try: + protocol = cloud.identity.get_federation_protocol(idp, name) + except sdk.exceptions.ResourceNotFound: + protocol = None + except sdk.exceptions.OpenStackCloudException as ex: + module.fail_json(msg='Failed to get protocol: {0}'.format(str(ex))) + + if state == 'absent': + if protocol is not None: + changed = delete_protocol(module, sdk, cloud, protocol) + module.exit_json(changed=changed) + + # state == 'present' + else: + if protocol is None: + if module.params.get('mapping_id') is None: + module.fail_json(msg='A mapping_id must be passed when creating' + ' a protocol') + (changed, protocol) = create_protocol(module, sdk, cloud, name) + protocol = normalize_protocol(protocol) + module.exit_json(changed=changed, protocol=protocol) + + else: + (changed, new_protocol) = update_protocol(module, sdk, cloud, protocol) + new_protocol = normalize_protocol(new_protocol) + module.exit_json(changed=changed, protocol=new_protocol) + + +if __name__ == '__main__': + main()