feat: introduce share_type modules
Add share_type and share_type_info modules. Uses direct Manila API calls via the SDK's session/connection interface since share type resources are not available in openstacksdk. Change-Id: I49af9a53435e226c5cc93a14190f85ef4637c798 Signed-off-by: Tadas Sutkaitis <tadasas@gmail.com>
This commit is contained in:
45
.zuul.yaml
45
.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:
|
||||
|
||||
5
ci/roles/share_type/defaults/main.yml
Normal file
5
ci/roles/share_type/defaults/main.yml
Normal file
@@ -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
|
||||
130
ci/roles/share_type/tasks/main.yml
Normal file
130
ci/roles/share_type/tasks/main.yml
Normal file
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -78,6 +78,8 @@ action_groups:
|
||||
- server_info
|
||||
- server_metadata
|
||||
- server_volume
|
||||
- share_type
|
||||
- share_type_info
|
||||
- stack
|
||||
- stack_info
|
||||
- subnet
|
||||
|
||||
520
plugins/modules/share_type.py
Normal file
520
plugins/modules/share_type.py
Normal file
@@ -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()
|
||||
239
plugins/modules/share_type_info.py
Normal file
239
plugins/modules/share_type_info.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user