diff --git a/.zuul.yaml b/.zuul.yaml index af60f13e..9af18286 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -95,6 +95,39 @@ c-bak: false tox_extra_args: -vv --skip-missing-interpreters=false -- coe_cluster coe_cluster_template +- job: + name: ansible-collections-openstack-functional-devstack-manila-base + parent: ansible-collections-openstack-functional-devstack-base + # Do not restrict branches in base jobs because else Zuul would not find a matching + # parent job variant during job freeze when child jobs are on other branches. + description: | + Run openstack collections functional tests against a devstack with Manila plugin enabled + # Do not set job.override-checkout or job.required-projects.override-checkout in base job because + # else Zuul will use this branch when matching variants for parent jobs during job freeze + required-projects: + - openstack/manila + - openstack/python-manilaclient + files: + - ^ci/roles/share_type/.*$ + - ^plugins/modules/share_type.py + - ^plugins/modules/share_type_info.py + timeout: 10800 + vars: + devstack_localrc: + MANILA_ENABLED_BACKENDS: generic + MANILA_OPTGROUP_generic_driver_handles_share_servers: true + MANILA_OPTGROUP_generic_connect_share_server_to_tenant_network: true + MANILA_USE_SERVICE_INSTANCE_PASSWORD: true + devstack_plugins: + manila: https://opendev.org/openstack/manila + devstack_services: + manila: true + m-api: true + m-sch: true + m-shr: true + m-dat: true + tox_extra_args: -vv --skip-missing-interpreters=false -- share_type share_type_info + - job: name: ansible-collections-openstack-functional-devstack-magnum parent: ansible-collections-openstack-functional-devstack-magnum-base @@ -104,6 +137,15 @@ with Magnum plugin enabled, using master of openstacksdk and latest ansible release. Run it only on coe_cluster{,_template} changes. +- job: + name: ansible-collections-openstack-functional-devstack-manila + parent: ansible-collections-openstack-functional-devstack-manila-base + branches: master + description: | + Run openstack collections functional tests against a master devstack + with Manila plugin enabled, using master of openstacksdk and latest + ansible release. Run it only on share_type{,_info} changes. + - job: name: ansible-collections-openstack-functional-devstack-octavia-base parent: ansible-collections-openstack-functional-devstack-base @@ -288,6 +330,7 @@ - ansible-collections-openstack-functional-devstack-ansible-2.18 - ansible-collections-openstack-functional-devstack-ansible-devel - ansible-collections-openstack-functional-devstack-magnum + - ansible-collections-openstack-functional-devstack-manila - ansible-collections-openstack-functional-devstack-octavia - bifrost-collections-src: @@ -303,6 +346,7 @@ - openstack-tox-linters-ansible-2.18 - ansible-collections-openstack-functional-devstack-releases - ansible-collections-openstack-functional-devstack-magnum + - ansible-collections-openstack-functional-devstack-manila - ansible-collections-openstack-functional-devstack-octavia periodic: @@ -316,6 +360,7 @@ - bifrost-collections-src - bifrost-keystone-collections-src - ansible-collections-openstack-functional-devstack-magnum + - ansible-collections-openstack-functional-devstack-manila - ansible-collections-openstack-functional-devstack-octavia tag: diff --git a/ci/roles/share_type/defaults/main.yml b/ci/roles/share_type/defaults/main.yml new file mode 100644 index 00000000..ced5eb45 --- /dev/null +++ b/ci/roles/share_type/defaults/main.yml @@ -0,0 +1,5 @@ +--- +share_backend_name: GENERIC_BACKEND +share_type_name: test_share_type +share_type_description: Test share type for CI +share_type_alt_description: Changed test share type diff --git a/ci/roles/share_type/tasks/main.yml b/ci/roles/share_type/tasks/main.yml new file mode 100644 index 00000000..eabea024 --- /dev/null +++ b/ci/roles/share_type/tasks/main.yml @@ -0,0 +1,130 @@ +--- +- name: Create share type + openstack.cloud.share_type: + name: "{{ share_type_name }}" + cloud: "{{ cloud }}" + state: present + extra_specs: + share_backend_name: "{{ share_backend_name }}" + snapshot_support: true + create_share_from_snapshot_support: true + description: "{{ share_type_description }}" + register: the_result + +- name: Check created share type + vars: + the_share_type: "{{ the_result.share_type }}" + ansible.builtin.assert: + that: + - "'id' in the_result.share_type" + - the_share_type.description == share_type_description + - the_share_type.is_public == True + - the_share_type.name == share_type_name + - the_share_type.extra_specs['share_backend_name'] == share_backend_name + - the_share_type.extra_specs['snapshot_support'] == "True" + - the_share_type.extra_specs['create_share_from_snapshot_support'] == "True" + success_msg: >- + Created share type: {{ the_result.share_type.id }}, + Name: {{ the_result.share_type.name }}, + Description: {{ the_result.share_type.description }} + +- name: Test share type info module + openstack.cloud.share_type_info: + name: "{{ share_type_name }}" + cloud: "{{ cloud }}" + register: info_result + +- name: Check share type info result + ansible.builtin.assert: + that: + - info_result.share_type.id == the_result.share_type.id + - info_result.share_type.name == share_type_name + - info_result.share_type.description == share_type_description + success_msg: "Share type info retrieved successfully" + +- name: Test, check idempotency + openstack.cloud.share_type: + name: "{{ share_type_name }}" + cloud: "{{ cloud }}" + state: present + extra_specs: + share_backend_name: "{{ share_backend_name }}" + snapshot_support: true + create_share_from_snapshot_support: true + description: "{{ share_type_description }}" + is_public: true + register: the_result + +- name: Check result.changed is false + ansible.builtin.assert: + that: + - the_result.changed == false + success_msg: "Request with the same details lead to no changes" + +- name: Add extra spec + openstack.cloud.share_type: + cloud: "{{ cloud }}" + name: "{{ share_type_name }}" + state: present + extra_specs: + share_backend_name: "{{ share_backend_name }}" + snapshot_support: true + create_share_from_snapshot_support: true + some_spec: fake_spec + description: "{{ share_type_alt_description }}" + is_public: true + register: the_result + +- name: Check share type extra spec + ansible.builtin.assert: + that: + - "'some_spec' in the_result.share_type.extra_specs" + - the_result.share_type.extra_specs["some_spec"] == "fake_spec" + - the_result.share_type.description == share_type_alt_description + success_msg: >- + New extra specs: {{ the_result.share_type.extra_specs }} + +- name: Remove extra spec by updating with reduced set + openstack.cloud.share_type: + cloud: "{{ cloud }}" + name: "{{ share_type_name }}" + state: present + extra_specs: + share_backend_name: "{{ share_backend_name }}" + snapshot_support: true + create_share_from_snapshot_support: true + description: "{{ share_type_alt_description }}" + is_public: true + register: the_result + +- name: Check extra spec was removed + ansible.builtin.assert: + that: + - "'some_spec' not in the_result.share_type.extra_specs" + success_msg: "Extra spec was successfully removed" + +- name: Delete share type + openstack.cloud.share_type: + cloud: "{{ cloud }}" + name: "{{ share_type_name }}" + state: absent + register: the_result + +- name: Check deletion was successful + ansible.builtin.assert: + that: + - the_result.changed == true + success_msg: "Share type deleted successfully" + +- name: Test deletion idempotency + openstack.cloud.share_type: + cloud: "{{ cloud }}" + name: "{{ share_type_name }}" + state: absent + register: the_result + +- name: Check deletion idempotency + ansible.builtin.assert: + that: + - the_result.changed == false + success_msg: "Deletion idempotency works correctly" diff --git a/ci/run-ansible-tests-collection.sh b/ci/run-ansible-tests-collection.sh index c9355ab8..cd31cf13 100755 --- a/ci/run-ansible-tests-collection.sh +++ b/ci/run-ansible-tests-collection.sh @@ -124,6 +124,11 @@ if [ ! -e /etc/magnum ]; then tag_opt+=" --skip-tags coe_cluster,coe_cluster_template" fi +if ! systemctl is-enabled devstack@m-api.service 2>&1; then + # Skip share_type tasks if Manila is not available + tag_opt+=" --skip-tags share_type" +fi + cd ci/ # Run tests diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 95ec9076..6ced59e6 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -53,6 +53,7 @@ - { role: server_group, tags: server_group } - { role: server_metadata, tags: server_metadata } - { role: server_volume, tags: server_volume } + - { role: share_type, tags: share_type } - { role: stack, tags: stack } - { role: subnet, tags: subnet } - { role: subnet_pool, tags: subnet_pool } diff --git a/meta/runtime.yml b/meta/runtime.yml index 70128c4f..0e1d208e 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -78,6 +78,8 @@ action_groups: - server_info - server_metadata - server_volume + - share_type + - share_type_info - stack - stack_info - subnet diff --git a/plugins/modules/share_type.py b/plugins/modules/share_type.py new file mode 100644 index 00000000..04036cd1 --- /dev/null +++ b/plugins/modules/share_type.py @@ -0,0 +1,520 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 VEXXHOST, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: share_type +short_description: Manage OpenStack share type +author: OpenStack Ansible SIG +description: + - Add, remove or update share types in OpenStack Manila. +options: + name: + description: + - Share type name or id. + - For private share types, the UUID must be used instead of name. + required: true + type: str + description: + description: + - Description of the share type. + type: str + extra_specs: + description: + - Dictionary of share type extra specifications + type: dict + is_public: + description: + - Make share type accessible to the public. + - Can be updated after creation using Manila API direct updates. + type: bool + default: true + driver_handles_share_servers: + description: + - Boolean flag indicating whether share servers are managed by the driver. + - Required for share type creation. + - This is automatically added to extra_specs as 'driver_handles_share_servers'. + type: bool + default: true + state: + description: + - Indicate desired state of the resource. + choices: ['present', 'absent'] + default: present + type: str +extends_documentation_fragment: + - openstack.cloud.openstack +""" + +EXAMPLES = r""" + - name: Delete share type by name + openstack.cloud.share_type: + name: test_share_type + state: absent + + - name: Delete share type by id + openstack.cloud.share_type: + name: fbadfa6b-5f17-4c26-948e-73b94de57b42 + state: absent + + - name: Create share type + openstack.cloud.share_type: + name: manila-generic-share + state: present + driver_handles_share_servers: true + extra_specs: + share_backend_name: GENERIC_BACKEND + snapshot_support: true + create_share_from_snapshot_support: true + description: Generic share type + is_public: true +""" + +RETURN = """ +share_type: + description: Dictionary describing share type + returned: On success when I(state) is 'present' + type: dict + contains: + name: + description: share type name + returned: success + type: str + sample: manila-generic-share + extra_specs: + description: share type extra specifications + returned: success + type: dict + sample: {"share_backend_name": "GENERIC_BACKEND", "snapshot_support": "true"} + is_public: + description: whether the share type is public + returned: success + type: bool + sample: True + description: + description: share type description + returned: success + type: str + sample: Generic share type + driver_handles_share_servers: + description: whether driver handles share servers + returned: success + type: bool + sample: true + id: + description: share type uuid + returned: success + type: str + sample: b75d8c5c-a6d8-4a5d-8c86-ef4f1298525d +""" + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule, +) + +# Manila API microversion 2.50 provides complete share type information +# including is_default field and description +# Reference: https://docs.openstack.org/api-ref/shared-file-system/#show-share-type-detail +MANILA_MICROVERSION = "2.50" + + +class ShareTypeModule(OpenStackModule): + argument_spec = dict( + name=dict(type="str", required=True), + description=dict(type="str", required=False), + extra_specs=dict(type="dict", required=False), + is_public=dict(type="bool", default=True), + driver_handles_share_servers=dict(type="bool", default=True), + state=dict(type="str", default="present", choices=["absent", "present"]), + ) + module_kwargs = dict( + required_if=[("state", "present", ["driver_handles_share_servers"])], + supports_check_mode=True, + ) + + @staticmethod + def _extract_result(details): + if details is not None: + if hasattr(details, "to_dict"): + result = details.to_dict(computed=False) + elif isinstance(details, dict): + result = details.copy() + else: + result = dict(details) if details else {} + + # Normalize is_public field from API response + if result and "os-share-type-access:is_public" in result: + result["is_public"] = result["os-share-type-access:is_public"] + elif result and "share_type_access:is_public" in result: + result["is_public"] = result["share_type_access:is_public"] + + return result + return {} + + def _find_share_type(self, name_or_id): + """ + Find share type by name or ID with comprehensive information. + + Uses direct Manila API calls since SDK methods are not available. + Handles both public and private share types. + """ + # Try direct access first for complete information + share_type = self._find_by_direct_access(name_or_id) + if share_type: + return share_type + + # If direct access fails, try searching in public listing + # This handles cases where we have the name but need to find the ID + try: + response = self.conn.shared_file_system.get("/types") + share_types = response.json().get("share_types", []) + + for share_type in share_types: + if share_type["name"] == name_or_id or share_type["id"] == name_or_id: + # Found by name, now get complete info using the ID + result = self._find_by_direct_access(share_type["id"]) + if result: + return result + except Exception: + pass + + return None + + def _find_by_direct_access(self, name_or_id): + """ + Find share type by direct access using Manila API. + + Uses microversion to get complete information including description and is_default. + Falls back to basic API if microversion is not supported. + """ + # Try with microversion first for complete information + try: + response = self.conn.shared_file_system.get( + f"/types/{name_or_id}", microversion=MANILA_MICROVERSION + ) + share_type_data = response.json().get("share_type", {}) + if share_type_data: + return share_type_data + except Exception: + pass + + # Fallback: try without microversion for basic information + try: + response = self.conn.shared_file_system.get(f"/types/{name_or_id}") + share_type_data = response.json().get("share_type", {}) + if share_type_data: + return share_type_data + except Exception: + pass + + return None + + def run(self): + state = self.params["state"] + name_or_id = self.params["name"] + + # Find existing share type (similar to volume_type.py pattern) + share_type = self._find_share_type(name_or_id) + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, share_type)) + + if state == "present" and not share_type: + # Create type + create_result = self._create() + share_type = self._extract_result(create_result) + self.exit_json(changed=True, share_type=share_type) + + elif state == "present" and share_type: + # Update type + update = self._build_update(share_type) + update_result = self._update(share_type, update) + share_type = self._extract_result(update_result) + self.exit_json(changed=bool(update), share_type=share_type) + + elif state == "absent" and share_type: + # Delete type + self._delete(share_type) + self.exit_json(changed=True) + + else: + # state == 'absent' and not share_type + self.exit_json(changed=False) + + def _build_update(self, share_type): + return { + **self._build_update_extra_specs(share_type), + **self._build_update_share_type(share_type), + } + + def _build_update_extra_specs(self, share_type): + update = {} + + old_extra_specs = share_type.get("extra_specs", {}) + + # Build the complete new extra specs including driver_handles_share_servers + new_extra_specs = {} + + # Add driver_handles_share_servers (always required) + if self.params.get("driver_handles_share_servers") is not None: + new_extra_specs["driver_handles_share_servers"] = str( + self.params["driver_handles_share_servers"] + ).title() + + # Add user-defined extra specs + if self.params.get("extra_specs"): + new_extra_specs.update( + {k: str(v) for k, v in self.params["extra_specs"].items()} + ) + + delete_extra_specs_keys = set(old_extra_specs.keys()) - set( + new_extra_specs.keys() + ) + + if delete_extra_specs_keys: + update["delete_extra_specs_keys"] = delete_extra_specs_keys + + if old_extra_specs != new_extra_specs: + update["create_extra_specs"] = new_extra_specs + + return update + + def _build_update_share_type(self, share_type): + update = {} + # Only allow description updates - name is used for identification + allowed_attributes = ["description"] + + # Handle is_public updates - CLI supports this, so we should too + # Always check is_public since it has a default value of True + current_is_public = share_type.get( + "os-share-type-access:is_public", + share_type.get("share_type_access:is_public"), + ) + requested_is_public = self.params["is_public"] # Will be True by default now + if current_is_public != requested_is_public: + # Mark this as needing a special access update + update["update_access"] = { + "is_public": requested_is_public, + "share_type_id": share_type.get("id"), + } + + type_attributes = { + k: self.params[k] + for k in allowed_attributes + if k in self.params + and self.params.get(k) is not None + and self.params.get(k) != share_type.get(k) + } + + if type_attributes: + update["type_attributes"] = type_attributes + + return update + + def _create(self): + share_type_attrs = {"name": self.params["name"]} + + if self.params.get("description") is not None: + share_type_attrs["description"] = self.params["description"] + + # Handle driver_handles_share_servers - this is the key required parameter + extra_specs = {} + if self.params.get("driver_handles_share_servers") is not None: + extra_specs["driver_handles_share_servers"] = str( + self.params["driver_handles_share_servers"] + ).title() + + # Add user-defined extra specs + if self.params.get("extra_specs"): + extra_specs.update( + {k: str(v) for k, v in self.params["extra_specs"].items()} + ) + + if extra_specs: + share_type_attrs["extra_specs"] = extra_specs + + # Handle is_public parameter - field name depends on API version + if self.params.get("is_public") is not None: + # For microversion (API 2.7+), use share_type_access:is_public + # For older versions, use os-share-type-access:is_public + share_type_attrs["share_type_access:is_public"] = self.params["is_public"] + # Also include legacy field for compatibility + share_type_attrs["os-share-type-access:is_public"] = self.params[ + "is_public" + ] + + try: + payload = {"share_type": share_type_attrs} + + # Try with microversion first (supports share_type_access:is_public) + try: + response = self.conn.shared_file_system.post( + "/types", json=payload, microversion=MANILA_MICROVERSION + ) + share_type_data = response.json().get("share_type", {}) + except Exception: + # Fallback: try without microversion (uses os-share-type-access:is_public) + # Remove the newer field name for older API compatibility + if "share_type_access:is_public" in share_type_attrs: + del share_type_attrs["share_type_access:is_public"] + payload = {"share_type": share_type_attrs} + response = self.conn.shared_file_system.post("/types", json=payload) + share_type_data = response.json().get("share_type", {}) + + return share_type_data + + except Exception as e: + self.fail_json(msg=f"Failed to create share type: {str(e)}") + + def _delete(self, share_type): + # Use direct API call since SDK method may not exist + try: + share_type_id = ( + share_type.get("id") if isinstance(share_type, dict) else share_type.id + ) + # Try with microversion first, fallback if not supported + try: + self.conn.shared_file_system.delete( + f"/types/{share_type_id}", microversion=MANILA_MICROVERSION + ) + except Exception: + self.conn.shared_file_system.delete(f"/types/{share_type_id}") + except Exception as e: + self.fail_json(msg=f"Failed to delete share type: {str(e)}") + + def _update(self, share_type, update): + if not update: + return share_type + share_type = self._update_share_type(share_type, update) + share_type = self._update_extra_specs(share_type, update) + share_type = self._update_access(share_type, update) + return share_type + + def _update_extra_specs(self, share_type, update): + share_type_id = ( + share_type.get("id") if isinstance(share_type, dict) else share_type.id + ) + + delete_extra_specs_keys = update.get("delete_extra_specs_keys") + if delete_extra_specs_keys: + for key in delete_extra_specs_keys: + try: + # Try with microversion first, fallback if not supported + try: + self.conn.shared_file_system.delete( + f"/types/{share_type_id}/extra_specs/{key}", + microversion=MANILA_MICROVERSION, + ) + except Exception: + self.conn.shared_file_system.delete( + f"/types/{share_type_id}/extra_specs/{key}" + ) + except Exception as e: + self.fail_json(msg=f"Failed to delete extra spec '{key}': {str(e)}") + # refresh share_type information + share_type = self._find_share_type(share_type_id) + + create_extra_specs = update.get("create_extra_specs") + if create_extra_specs: + # Convert values to strings as Manila API expects string values + string_specs = {k: str(v) for k, v in create_extra_specs.items()} + try: + # Try with microversion first, fallback if not supported + try: + self.conn.shared_file_system.post( + f"/types/{share_type_id}/extra_specs", + json={"extra_specs": string_specs}, + microversion=MANILA_MICROVERSION, + ) + except Exception: + self.conn.shared_file_system.post( + f"/types/{share_type_id}/extra_specs", + json={"extra_specs": string_specs}, + ) + except Exception as e: + self.fail_json(msg=f"Failed to update extra specs: {str(e)}") + # refresh share_type information + share_type = self._find_share_type(share_type_id) + + return share_type + + def _update_access(self, share_type, update): + """Update share type access (public/private) using direct API update""" + access_update = update.get("update_access") + if not access_update: + return share_type + + share_type_id = access_update["share_type_id"] + is_public = access_update["is_public"] + + try: + # Use direct update with share_type_access:is_public (works for both public and private) + update_payload = {"share_type": {"share_type_access:is_public": is_public}} + + try: + self.conn.shared_file_system.put( + f"/types/{share_type_id}", + json=update_payload, + microversion=MANILA_MICROVERSION, + ) + except Exception: + # Fallback: try with legacy field name for older API versions + update_payload = { + "share_type": {"os-share-type-access:is_public": is_public} + } + self.conn.shared_file_system.put( + f"/types/{share_type_id}", json=update_payload + ) + + # Refresh share type information after access change + share_type = self._find_share_type(share_type_id) + + except Exception as e: + self.fail_json(msg=f"Failed to update share type access: {str(e)}") + + return share_type + + def _update_share_type(self, share_type, update): + type_attributes = update.get("type_attributes") + if type_attributes: + share_type_id = ( + share_type.get("id") if isinstance(share_type, dict) else share_type.id + ) + try: + # Try with microversion first, fallback if not supported + try: + response = self.conn.shared_file_system.put( + f"/types/{share_type_id}", + json={"share_type": type_attributes}, + microversion=MANILA_MICROVERSION, + ) + except Exception: + response = self.conn.shared_file_system.put( + f"/types/{share_type_id}", json={"share_type": type_attributes} + ) + updated_type = response.json().get("share_type", {}) + return updated_type + except Exception as e: + self.fail_json(msg=f"Failed to update share type: {str(e)}") + return share_type + + def _will_change(self, state, share_type): + if state == "present" and not share_type: + return True + if state == "present" and share_type: + return bool(self._build_update(share_type)) + if state == "absent" and share_type: + return True + return False + + +def main(): + module = ShareTypeModule() + module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/share_type_info.py b/plugins/modules/share_type_info.py new file mode 100644 index 00000000..cd145fb4 --- /dev/null +++ b/plugins/modules/share_type_info.py @@ -0,0 +1,239 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 VEXXHOST, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: share_type_info +short_description: Get OpenStack share type details +author: OpenStack Ansible SIG +description: + - Get share type details in OpenStack Manila. + - Get share type access details for private share types. + - Uses Manila API microversion 2.50 to retrieve complete share type information including is_default field. + - Safely falls back to basic information if microversion 2.50 is not supported by the backend. + - Private share types can only be accessed by UUID. +options: + name: + description: + - Share type name or id. + - For private share types, the UUID must be used instead of name. + required: true + type: str +extends_documentation_fragment: + - openstack.cloud.openstack +""" + +EXAMPLES = r""" + - name: Get share type details + openstack.cloud.share_type_info: + name: manila-generic-share + + - name: Get share type details by id + openstack.cloud.share_type_info: + name: fbadfa6b-5f17-4c26-948e-73b94de57b42 +""" + +RETURN = """ +share_type: + description: Dictionary describing share type + returned: On success + type: dict + contains: + id: + description: share type uuid + returned: success + type: str + sample: 59575cfc-3582-4efc-8eee-f47fcb25ea6b + name: + description: share type name + returned: success + type: str + sample: default + description: + description: + - share type description + - Available when Manila API microversion 2.50 is supported + - Falls back to empty string if microversion is not available + returned: success + type: str + sample: "Default Manila share type" + is_default: + description: + - whether this is the default share type + - Retrieved from the API response when microversion 2.50 is supported + - Falls back to null if microversion is not available or field is not present + returned: success + type: bool + sample: true + is_public: + description: whether the share type is public (true) or private (false) + returned: success + type: bool + sample: true + required_extra_specs: + description: Required extra specifications for the share type + returned: success + type: dict + sample: {"driver_handles_share_servers": "True"} + optional_extra_specs: + description: Optional extra specifications for the share type + returned: success + type: dict + sample: {"snapshot_support": "True", "create_share_from_snapshot_support": "True"} +""" + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule, +) + +# Manila API microversion 2.50 provides complete share type information +# including is_default field and description +# Reference: https://docs.openstack.org/api-ref/shared-file-system/#show-share-type-detail +MANILA_MICROVERSION = "2.50" + + +class ShareTypeInfoModule(OpenStackModule): + argument_spec = dict(name=dict(type="str", required=True)) + module_kwargs = dict( + supports_check_mode=True, + ) + + def __init__(self, **kwargs): + super(ShareTypeInfoModule, self).__init__(**kwargs) + + def _find_share_type(self, name_or_id): + """ + Find share type by name or ID with comprehensive information. + """ + share_type = self._find_by_direct_access(name_or_id) + if share_type: + return share_type + + # If direct access fails, try searching in public listing + # This handles cases where we have the name but need to find the ID + try: + response = self.conn.shared_file_system.get("/types") + share_types = response.json().get("share_types", []) + + for share_type in share_types: + if share_type["name"] == name_or_id or share_type["id"] == name_or_id: + # Found by name, now get complete info using the ID + result = self._find_by_direct_access(share_type["id"]) + if result: + return result + except Exception: + pass + + return None + + def _find_by_direct_access(self, name_or_id): + """ + Find share type by direct access (for private share types). + """ + try: + response = self.conn.shared_file_system.get( + f"/types/{name_or_id}", microversion=MANILA_MICROVERSION + ) + share_type_data = response.json().get("share_type", {}) + if share_type_data: + return share_type_data + except Exception: + pass + + # Fallback: try without microversion for basic information + try: + response = self.conn.shared_file_system.get(f"/types/{name_or_id}") + share_type_data = response.json().get("share_type", {}) + if share_type_data: + return share_type_data + except Exception: + pass + + return None + + def _normalize_share_type_dict(self, share_type_dict): + """ + Normalize share type dictionary to match CLI output format. + """ + # Extract extra specs information + extra_specs = share_type_dict.get("extra_specs", {}) + required_extra_specs = share_type_dict.get("required_extra_specs", {}) + + # Optional extra specs are those in extra_specs but not in required_extra_specs + optional_extra_specs = { + key: value + for key, value in extra_specs.items() + if key not in required_extra_specs + } + + # Determine if this is the default share type + # Use the is_default field from API response (available with microversion 2.50) + # If not available (older API versions), default to None + is_default = share_type_dict.get("is_default", None) + + # Handle the description field - available through microversion 2.50 + # Convert None to empty string if API returns null + description = share_type_dict.get("description") or "" + + # Determine visibility - check both new and legacy field names + # Use the same logic as share_type.py for consistency + is_public = share_type_dict.get( + "os-share-type-access:is_public", + share_type_dict.get("share_type_access:is_public"), + ) + + # Build the normalized dictionary matching CLI output + normalized = { + "id": share_type_dict.get("id"), + "name": share_type_dict.get("name"), + "is_public": is_public, + "is_default": is_default, + "required_extra_specs": required_extra_specs, + "optional_extra_specs": optional_extra_specs, + "description": description, + } + + return normalized + + def run(self): + """ + Main execution method following OpenStackModule pattern. + + Retrieves share type information using Manila API microversion for complete + details including description and is_default fields. Falls back gracefully to + basic API calls if microversion is not supported by the backend. + """ + name_or_id = self.params["name"] + + share_type = self._find_share_type(name_or_id) + if not share_type: + self.fail_json( + msg=f"Share type '{name_or_id}' not found. " + f"If this is a private share type, use its UUID instead of name." + ) + + if hasattr(share_type, "to_dict"): + share_type_dict = share_type.to_dict() + elif isinstance(share_type, dict): + share_type_dict = share_type + else: + share_type_dict = dict(share_type) if share_type else {} + + # Normalize the output to match CLI format + normalized_share_type = self._normalize_share_type_dict(share_type_dict) + + # Return results in the standard format + result = dict(changed=False, share_type=normalized_share_type) + return result + + +def main(): + module = ShareTypeInfoModule() + module() + + +if __name__ == "__main__": + main()