From 556208fc3cf6641e362b30efe9bd82f70a4e73ff Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Wed, 16 Apr 2025 11:31:39 -0400 Subject: [PATCH] Add volume_manage module This module introduces the ability to use the cinder manage and unmanage of an existing volume on a cinder backend. Due to API limitations, when unmanaging a volume, only the volume ID can be provided. Change-Id: If969f198864e6bd65dbb9fce4923af1674da34bc --- ci/roles/volume_manage/defaults/main.yml | 32 +++ ci/roles/volume_manage/tasks/main.yml | 65 +++++ ci/run-collection.yml | 1 + meta/runtime.yml | 1 + plugins/modules/volume_manage.py | 309 +++++++++++++++++++++++ 5 files changed, 408 insertions(+) create mode 100644 ci/roles/volume_manage/defaults/main.yml create mode 100644 ci/roles/volume_manage/tasks/main.yml create mode 100644 plugins/modules/volume_manage.py diff --git a/ci/roles/volume_manage/defaults/main.yml b/ci/roles/volume_manage/defaults/main.yml new file mode 100644 index 00000000..a3f5a0dc --- /dev/null +++ b/ci/roles/volume_manage/defaults/main.yml @@ -0,0 +1,32 @@ +test_volume: ansible_test_volume +managed_volume: managed_test_volume +expected_fields: + - attachments + - availability_zone + - consistency_group_id + - created_at + - updated_at + - description + - extended_replication_status + - group_id + - host + - image_id + - is_bootable + - is_encrypted + - is_multiattach + - migration_id + - migration_status + - project_id + - replication_driver_data + - replication_status + - scheduler_hints + - size + - snapshot_id + - source_volume_id + - status + - user_id + - volume_image_metadata + - volume_type + - id + - name + - metadata diff --git a/ci/roles/volume_manage/tasks/main.yml b/ci/roles/volume_manage/tasks/main.yml new file mode 100644 index 00000000..9bbe7706 --- /dev/null +++ b/ci/roles/volume_manage/tasks/main.yml @@ -0,0 +1,65 @@ +--- +- name: Create volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: present + size: 1 + name: "{{ test_volume }}" + description: Test volume + register: vol + +- assert: + that: item in vol.volume + loop: "{{ expected_fields }}" + +- name: Unmanage volume + openstack.cloud.volume_manage: + cloud: "{{ cloud }}" + state: absent + name: "{{ vol.volume.id }}" + +- name: Unmanage volume again + openstack.cloud.volume_manage: + cloud: "{{ cloud }}" + state: absent + name: "{{ vol.volume.id }}" + register: unmanage_idempotency + +- assert: + that: + - unmanage_idempotency is not changed + +- name: Manage volume + openstack.cloud.volume_manage: + cloud: "{{ cloud }}" + state: present + source_name: volume-{{ vol.volume.id }} + host: "{{ vol.volume.host }}" + name: "{{ managed_volume }}" + register: new_vol + +- assert: + that: + - new_vol.volume.name == "{{ managed_volume }}" + +- name: Manage volume again + openstack.cloud.volume_manage: + cloud: "{{ cloud }}" + state: present + source_name: volume-{{ vol.volume.id }} + host: "{{ vol.volume.host }}" + name: "{{ managed_volume }}" + register: vol_idempotency + +- assert: + that: + - vol_idempotency is not changed + +- pause: + seconds: 10 + +- name: Delete volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: absent + name: "{{ managed_volume }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 4ff6ad8a..95ec9076 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -59,6 +59,7 @@ - { role: volume, tags: volume } - { role: volume_type, tags: volume_type } - { role: volume_backup, tags: volume_backup } + - { role: volume_manage, tags: volume_manage } - { role: volume_service, tags: volume_service } - { role: volume_snapshot, tags: volume_snapshot } - { role: volume_type_access, tags: volume_type_access } diff --git a/meta/runtime.yml b/meta/runtime.yml index e7480b78..70128c4f 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -85,6 +85,7 @@ action_groups: - subnets_info - trunk - volume + - volume_manage - volume_backup - volume_backup_info - volume_info diff --git a/plugins/modules/volume_manage.py b/plugins/modules/volume_manage.py new file mode 100644 index 00000000..06b5535b --- /dev/null +++ b/plugins/modules/volume_manage.py @@ -0,0 +1,309 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 by Pure Storage, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: volume_manage +short_description: Manage/Unmanage Volumes +author: OpenStack Ansible SIG +description: + - Manage or Unmanage Volume in OpenStack. +options: + description: + description: + - String describing the volume + type: str + metadata: + description: Metadata for the volume + type: dict + name: + description: + - Name of the volume to be unmanaged or + the new name of a managed volume + - When I(state) is C(absent) this must be + the cinder volume ID + required: true + type: str + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + type: str + bootable: + description: + - Bootable flag for volume. + type: bool + default: False + volume_type: + description: + - Volume type for volume + type: str + availability_zone: + description: + - The availability zone. + type: str + host: + description: + - Cinder host on which the existing volume resides + - Takes the form "host@backend-name#pool" + - Required when I(state) is C(present). + type: str + source_name: + description: + - Name of existing volume + type: str + source_id: + description: + - Identifier of existing volume + type: str +extends_documentation_fragment: +- openstack.cloud.openstack +""" + +RETURN = r""" +volume: + description: Cinder's representation of the volume object + returned: always + type: dict + contains: + attachments: + description: Instance attachment information. For a amanaged volume, this + will always be empty. + type: list + availability_zone: + description: The name of the availability zone. + type: str + consistency_group_id: + description: The UUID of the consistency group. + type: str + created_at: + description: The date and time when the resource was created. + type: str + description: + description: The volume description. + type: str + extended_replication_status: + description: Extended replication status on this volume. + type: str + group_id: + description: The ID of the group. + type: str + host: + description: The volume's current back-end. + type: str + id: + description: The UUID of the volume. + type: str + image_id: + description: Image on which the volume was based + type: str + is_bootable: + description: Enables or disables the bootable attribute. You can boot an + instance from a bootable volume. + type: str + is_encrypted: + description: If true, this volume is encrypted. + type: bool + is_multiattach: + description: Whether this volume can be attached to more than one + server. + type: bool + metadata: + description: A metadata object. Contains one or more metadata key and + value pairs that are associated with the volume. + type: dict + migration_id: + description: The volume ID that this volume name on the backend is + based on. + type: str + migration_status: + description: The status of this volume migration (None means that a + migration is not currently in progress). + type: str + name: + description: The volume name. + type: str + project_id: + description: The project ID which the volume belongs to. + type: str + replication_driver_data: + description: Data set by the replication driver + type: str + replication_status: + description: The volume replication status. + type: str + scheduler_hints: + description: Scheduler hints for the volume + type: dict + size: + description: The size of the volume, in gibibytes (GiB). + type: int + snapshot_id: + description: To create a volume from an existing snapshot, specify the + UUID of the volume snapshot. The volume is created in same + availability zone and with same size as the snapshot. + type: str + source_volume_id: + description: The UUID of the source volume. The API creates a new volume + with the same size as the source volume unless a larger size + is requested. + type: str + status: + description: The volume status. + type: str + updated_at: + description: The date and time when the resource was updated. + type: str + user_id: + description: The UUID of the user. + type: str + volume_image_metadata: + description: List of image metadata entries. Only included for volumes + that were created from an image, or from a snapshot of a + volume originally created from an image. + type: dict + volume_type: + description: The associated volume type name for the volume. + type: str +""" + +EXAMPLES = r""" +- name: Manage volume + openstack.cloud.volume_manage: + name: newly-managed-vol + source_name: manage-me + host: host@backend-name#pool + +- name: Unmanage volume + openstack.cloud.volume_manage: + name: "5c831866-3bb3-4d67-a7d3-1b90880c9d18" + state: absent +""" + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule, +) + + +class VolumeManageModule(OpenStackModule): + + argument_spec = dict( + description=dict(type="str"), + metadata=dict(type="dict"), + source_name=dict(type="str"), + source_id=dict(type="str"), + availability_zone=dict(type="str"), + host=dict(type="str"), + bootable=dict(default="false", type="bool"), + volume_type=dict(type="str"), + name=dict(required=True, type="str"), + state=dict( + default="present", choices=["absent", "present"], type="str" + ), + ) + + module_kwargs = dict( + required_if=[("state", "present", ["host"])], + supports_check_mode=True, + ) + + def run(self): + name = self.params["name"] + state = self.params["state"] + changed = False + + if state == "present": + changed = True + if not self.ansible.check_mode: + volumes = self._manage_list() + manageable = volumes["manageable-volumes"] + safe_to_manage = self._is_safe_to_manage( + manageable, self.params["source_name"] + ) + if not safe_to_manage: + self.exit_json(changed=False) + volume = self._manage() + if volume: + self.exit_json( + changed=changed, volume=volume.to_dict(computed=False) + ) + else: + self.exit_json(changed=False) + else: + self.exit_json(changed=changed) + + else: + volume = self.conn.block_storage.find_volume(name) + if volume: + changed = True + if not self.ansible.check_mode: + self._unmanage() + self.exit_json(changed=changed) + else: + self.exit_json(changed=changed) + + def _is_safe_to_manage(self, manageable_list, target_name): + entry = next( + ( + v + for v in manageable_list + if isinstance(v.get("reference"), dict) + and ( + v["reference"].get("name") == target_name + or v["reference"].get("source-name") == target_name + ) + ), + None, + ) + if entry is None: + return False + return entry.get("safe_to_manage", False) + + def _manage(self): + kwargs = { + key: self.params[key] + for key in [ + "description", + "bootable", + "volume_type", + "availability_zone", + "host", + "metadata", + "name", + ] + if self.params.get(key) is not None + } + kwargs["ref"] = {} + if self.params["source_name"]: + kwargs["ref"]["source-name"] = self.params["source_name"] + if self.params["source_id"]: + kwargs["ref"]["source-id"] = self.params["source_id"] + + volume = self.conn.block_storage.manage_volume(**kwargs) + + return volume + + def _manage_list(self): + response = self.conn.block_storage.get( + "/manageable_volumes?host=" + self.params["host"], + microversion="3.8", + ) + response.raise_for_status() + manageable_volumes = response.json() + return manageable_volumes + + def _unmanage(self): + self.conn.block_storage.unmanage_volume(self.params["name"]) + + +def main(): + module = VolumeManageModule() + module() + + +if __name__ == "__main__": + main()