diff --git a/ci/roles/auth/defaults/main.yml b/ci/roles/auth/defaults/main.yml new file mode 100644 index 00000000..746a0e6d --- /dev/null +++ b/ci/roles/auth/defaults/main.yml @@ -0,0 +1,2 @@ +expected_fields: + - auth_token diff --git a/ci/roles/auth/tasks/main.yml b/ci/roles/auth/tasks/main.yml index 3d93609c..891439ce 100644 --- a/ci/roles/auth/tasks/main.yml +++ b/ci/roles/auth/tasks/main.yml @@ -2,5 +2,10 @@ - name: Authenticate to the cloud openstack.cloud.auth: cloud={{ cloud }} + register: auth -- debug: var=service_catalog +- name: Assert return values of auth module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(auth.keys())|length == 0 diff --git a/ci/roles/catalog_service/defaults/main.yml b/ci/roles/catalog_service/defaults/main.yml new file mode 100644 index 00000000..ba758ce9 --- /dev/null +++ b/ci/roles/catalog_service/defaults/main.yml @@ -0,0 +1,7 @@ +expected_fields: + - description + - id + - is_enabled + - links + - name + - type diff --git a/ci/roles/catalog_service/tasks/main.yml b/ci/roles/catalog_service/tasks/main.yml index 9872363c..f3fd4704 100644 --- a/ci/roles/catalog_service/tasks/main.yml +++ b/ci/roles/catalog_service/tasks/main.yml @@ -23,15 +23,8 @@ - name: Verify returned values assert: - that: - - item in service_test.service - loop: - - description - - id - - is_enabled - - links - - name - - type + that: item in service_test.service + loop: "{{ expected_fields }}" - name: Check if the service test was created successfully openstack.cloud.catalog_service: @@ -42,15 +35,8 @@ - name: Verify returned values assert: - that: - - item in service_created.service - loop: - - description - - id - - is_enabled - - links - - name - - type + that: item in service_created.service + loop: "{{ expected_fields }}" - name: Update service test openstack.cloud.catalog_service: @@ -61,12 +47,35 @@ name: test register: service_test -- name: Check if description and enabled were updated +- name: Check if description and is_enabled were updated assert: that: - service_test.service.description == "A new description" - not (service_test.service.is_enabled|bool) +- name: Get all services + openstack.cloud.catalog_service_info: + cloud: "{{ cloud }}" + register: services + +- name: Assert return values of catalog_service_info module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(services.services[0].keys())|length == 0 + +- name: Get service by name + openstack.cloud.catalog_service_info: + cloud: "{{ cloud }}" + name: test + register: services + +- name: Assert services returned by catalog_service_info module + assert: + that: + - services.services|length == 1 + - services.services[0].id == service_test.service.id + - name: Delete service test openstack.cloud.catalog_service: cloud: "{{ cloud }}" diff --git a/meta/runtime.yml b/meta/runtime.yml index b538b1a4..25c4f2ef 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -10,6 +10,7 @@ action_groups: - baremetal_port - baremetal_port_info - catalog_service + - catalog_service_info - coe_cluster - coe_cluster_template - compute_flavor diff --git a/plugins/modules/auth.py b/plugins/modules/auth.py index 807b4b04..c187f243 100644 --- a/plugins/modules/auth.py +++ b/plugins/modules/auth.py @@ -4,13 +4,13 @@ # Copyright (c) 2015 Hewlett-Packard Development Company, L.P. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: auth -short_description: Retrieve an auth token +short_description: Retrieve auth token from OpenStack cloud author: OpenStack Ansible SIG description: - - Retrieve an auth token from an OpenStack Cloud + - Retrieve auth token from OpenStack cloud requirements: - "python >= 3.6" - "openstacksdk" @@ -18,40 +18,26 @@ extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -- name: Authenticate to the cloud and retrieve the service catalog +EXAMPLES = r''' +- name: Authenticate to cloud and return auth token openstack.cloud.auth: cloud: rax-dfw - -- name: Show service catalog - debug: - var: service_catalog ''' -RETURN = ''' +RETURN = r''' auth_token: description: Openstack API Auth Token returned: success type: str -service_catalog: - description: A dictionary of available API endpoints - returned: success - type: dict ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule class AuthModule(OpenStackModule): - argument_spec = dict() - module_kwargs = dict() - def run(self): - self.exit_json( - changed=False, - ansible_facts=dict( - auth_token=self.conn.auth_token, - service_catalog=self.conn.service_catalog)) + self.exit_json(changed=False, + auth_token=self.conn.auth_token) def main(): diff --git a/plugins/modules/catalog_service.py b/plugins/modules/catalog_service.py index 24d2ba25..a1bfb945 100644 --- a/plugins/modules/catalog_service.py +++ b/plugins/modules/catalog_service.py @@ -4,41 +4,38 @@ # Copyright 2016 Sam Yaple # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: catalog_service -short_description: Manage OpenStack Identity services +short_description: Manage OpenStack services author: OpenStack Ansible SIG description: - - Create, update, or delete OpenStack Identity service. If a service - with the supplied name already exists, it will be updated with the - new description and enabled attributes. + - Create, update or delete a OpenStack service. options: name: description: - - Name of the service + - Name of the service. required: true type: str description: description: - - Description of the service + - Description of the service. type: str is_enabled: description: - - Is the service enabled + - Whether this service is enabled or not. type: bool - default: 'yes' aliases: ['enabled'] type: description: - - The type of service + - The type of service. required: true type: str aliases: ['service_type'] state: description: - - Should the resource be present or absent. - choices: [present, absent] + - Whether the service should be C(present) or C(absent). + choices: ['present', 'absent'] default: present type: str requirements: @@ -49,44 +46,37 @@ extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create a service for glance -- openstack.cloud.catalog_service: +EXAMPLES = r''' +- name: Create a service for glance + openstack.cloud.catalog_service: cloud: mycloud state: present name: glance type: image description: OpenStack Image Service -# Delete a service -- openstack.cloud.catalog_service: + +- name: Delete a service + openstack.cloud.catalog_service: cloud: mycloud state: absent name: glance type: image ''' -RETURN = ''' +RETURN = r''' service: description: Dictionary describing the service. returned: On success when I(state) is 'present' type: dict contains: - id: - description: Service ID. - type: str - sample: "3292f020780b4d5baf27ff7e1d224c44" - name: - description: Service name. - type: str - sample: "glance" - type: - description: Service type. - type: str - sample: "image" description: description: Service description. type: str sample: "OpenStack Image Service" + id: + description: Service ID. + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" is_enabled: description: Service status. type: bool @@ -95,20 +85,23 @@ service: description: Link of the service type: str sample: http://10.0.0.1/identity/v3/services/0ae87 -id: - description: The service ID. - returned: On success when I(state) is 'present' - type: str - sample: "3292f020780b4d5baf27ff7e1d224c44" + name: + description: Service name. + type: str + sample: "glance" + type: + description: Service type. + type: str + sample: "image" ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class IdentityCatalogServiceModule(OpenStackModule): +class CatalogServiceModule(OpenStackModule): argument_spec = dict( description=dict(), - is_enabled=dict(default=True, aliases=['enabled'], type='bool'), + is_enabled=dict(aliases=['enabled'], type='bool'), name=dict(required=True), type=dict(required=True, aliases=['service_type']), state=dict(default='present', choices=['absent', 'present']), @@ -118,78 +111,103 @@ class IdentityCatalogServiceModule(OpenStackModule): supports_check_mode=True ) - def _needs_update(self, service): - for parameter in ('is_enabled', 'description', 'type'): - if service[parameter] != self.params[parameter]: - return True - return False - - def _system_state_change(self, service): - state = self.params['state'] - if state == 'absent' and service: - return True - - if state == 'present': - if service is None: - return True - return self._needs_update(service) - - return False - def run(self): - description = self.params['description'] - enabled = self.params['is_enabled'] - name = self.params['name'] state = self.params['state'] - type = self.params['type'] - filters = {'name': name, 'type': type} - - services = list(self.conn.identity.services(**filters)) - - service = None - if len(services) > 1: - self.fail_json( - msg='Service name %s and type %s are not unique' - % (name, type)) - elif len(services) == 1: - service = services[0] + service = self._find() if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change(service)) + self.exit_json(changed=self._will_change(state, service)) - args = {'name': name, 'enabled': enabled, 'type': type} - if description: - args['description'] = description + if state == 'present' and not service: + # Create service + service = self._create() + self.exit_json(changed=True, + service=service.to_dict(computed=False)) - if state == 'present': - if service is None: - service = self.conn.identity.create_service(**args) - changed = True - else: - if self._needs_update(service): - # The self.conn.update_service calls get_service that - # checks if the service is duplicated or not. We don't need - # to do it here because it was already checked above - service = self.conn.identity.update_service(service, - **args) - changed = True - else: - changed = False - service = service.to_dict(computed=False) - self.exit_json(changed=changed, service=service, id=service['id']) + elif state == 'present' and service: + # Update service + update = self._build_update(service) + if update: + service = self._update(service, update) - elif state == 'absent': - if service is None: - changed = False - else: - self.conn.identity.delete_service(service) - changed = True - self.exit_json(changed=changed) + self.exit_json(changed=bool(update), + service=service.to_dict(computed=False)) + + elif state == 'absent' and service: + # Delete service + self._delete(service) + self.exit_json(changed=True) + + elif state == 'absent' and not service: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, service): + update = {} + + non_updateable_keys = [k for k in ['name'] + if self.params[k] is not None + and self.params[k] != service[k]] + + if non_updateable_keys: + self.fail_json(msg='Cannot update parameters {0}' + .format(non_updateable_keys)) + + attributes = dict((k, self.params[k]) + for k in ['description', 'is_enabled', 'type'] + if self.params[k] is not None + and self.params[k] != service[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + kwargs = dict((k, self.params[k]) + for k in ['description', 'is_enabled', 'name', 'type'] + if self.params[k] is not None) + + return self.conn.identity.create_service(**kwargs) + + def _delete(self, service): + self.conn.identity.delete_service(service.id) + + def _find(self): + kwargs = dict((k, self.params[k]) for k in ['name', 'type']) + matches = list(self.conn.identity.services(**kwargs)) + + if len(matches) > 1: + self.fail_json(msg='Found more a single service' + ' matching the given parameters.') + elif len(matches) == 1: + return matches[0] + else: # len(matches) == 0 + return None + + def _update(self, service, update): + attributes = update.get('attributes') + if attributes: + service = self.conn.identity.update_service(service.id, + **attributes) + + return service + + def _will_change(self, state, service): + if state == 'present' and not service: + return True + elif state == 'present' and service: + return bool(self._build_update(service)) + elif state == 'absent' and service: + return True + else: + # state == 'absent' and not service: + return False def main(): - module = IdentityCatalogServiceModule() + module = CatalogServiceModule() module() diff --git a/plugins/modules/catalog_service_info.py b/plugins/modules/catalog_service_info.py new file mode 100644 index 00000000..a3cfdb72 --- /dev/null +++ b/plugins/modules/catalog_service_info.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 by Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r''' +module: catalog_service_info +short_description: Retrieve information about services from OpenStack +author: OpenStack Ansible SIG +description: + - Retrieve information about services from OpenStack. +options: + name: + description: + - Name or ID of the service. + type: str +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = r''' +- name: Fetch all services + openstack.cloud.catalog_service_info: + cloud: devstack + +- name: Fetch a single service + openstack.cloud.catalog_service_info: + cloud: devstack + name: heat +''' + +RETURN = r''' +services: + description: List of dictionaries the services. + returned: always + type: list + elements: dict + contains: + id: + description: Service ID. + type: str + sample: "3292f020780b4d5baf27ff7e1d224c44" + name: + description: Service name. + type: str + sample: "glance" + type: + description: Service type. + type: str + sample: "image" + description: + description: Service description. + type: str + sample: "OpenStack Image Service" + is_enabled: + description: Service status. + type: bool + sample: True + links: + description: Link of the service + type: str + sample: http://10.0.0.1/identity/v3/services/0ae87 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule +) + + +class CatalogServiceInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def run(self): + name_or_id = self.params['name'] + + if name_or_id: + service = self.conn.identity.find_service(name_or_id) + services = [service] if service else [] + else: + services = self.conn.identity.services() + + self.exit_json(changed=False, + services=[s.to_dict(computed=False) for s in services]) + + +def main(): + module = CatalogServiceInfoModule() + module() + + +if __name__ == "__main__": + main()