diff --git a/ci/roles/volume_image_metadata/defaults/main.yml b/ci/roles/volume_image_metadata/defaults/main.yml new file mode 100644 index 00000000..6d7f6841 --- /dev/null +++ b/ci/roles/volume_image_metadata/defaults/main.yml @@ -0,0 +1,7 @@ +--- +volume_image_metadata_cloud: "{{ cloud | default(omit) }}" +volume_image_metadata_volume_name: test-image-metadata-volume +volume_image_metadata_size: 1 +volume_image_metadata: + disk_format: qcow2 + container_format: bare diff --git a/ci/roles/volume_image_metadata/tasks/main.yml b/ci/roles/volume_image_metadata/tasks/main.yml new file mode 100644 index 00000000..f77cbd03 --- /dev/null +++ b/ci/roles/volume_image_metadata/tasks/main.yml @@ -0,0 +1,103 @@ +--- +- name: Get available images + openstack.cloud.image_info: + cloud: "{{ volume_image_metadata_cloud }}" + register: image_info + +- name: Select test image + set_fact: + volume_image_metadata_image_id: >- + {{ + image_info.images + | selectattr('status', 'equalto', 'active') + | list + | first + | default({}) + }} + +- name: Assert an image is available for testing + assert: + that: + - volume_image_metadata_image_id.id is defined + fail_msg: "No active images available in the cloud for volume_image_metadata CI test" + +- name: Create a test volume from image + openstack.cloud.volume: + cloud: "{{ volume_image_metadata_cloud }}" + state: present + name: "{{ volume_image_metadata_volume_name }}" + image: "{{ volume_image_metadata_image_id.id }}" + size: "{{ volume_image_metadata_size }}" + register: created_volume + +- name: Assert volume was created + assert: + that: + - created_volume.volume is defined + - created_volume.volume.id is defined + +- name: Get volume details + openstack.cloud.volume_info: + cloud: "{{ volume_image_metadata_cloud }}" + name: "{{ volume_image_metadata_volume_name }}" + register: volume_info + +- name: Assert volume has image metadata + assert: + that: + - volume_info.volumes[0].volume_image_metadata is defined + - volume_info.volumes[0].volume_image_metadata | length > 0 + +# -------------------------------------------------------------------- +# Exercise new module +# -------------------------------------------------------------------- +- name: Set volume image metadata + openstack.cloud.volume_image_metadata: + cloud: "{{ volume_image_metadata_cloud }}" + volume: "{{ created_volume.volume.id }}" + image_metadata: "{{ volume_image_metadata }}" + register: image_meta_result + +- name: Assert image metadata changed + assert: + that: + - image_meta_result.changed | bool + +# -------------------------------------------------------------------- +# Idempotency check +# -------------------------------------------------------------------- +- name: Set volume image metadata again (idempotent) + openstack.cloud.volume_image_metadata: + cloud: "{{ volume_image_metadata_cloud }}" + volume: "{{ created_volume.volume.id }}" + image_metadata: "{{ volume_image_metadata }}" + register: image_meta_idempotent + +- name: Assert idempotent behavior + assert: + that: + - not image_meta_idempotent.changed | bool + +# -------------------------------------------------------------------- +# Verify metadata persisted +# -------------------------------------------------------------------- +- name: Re-fetch volume details + openstack.cloud.volume_info: + cloud: "{{ volume_image_metadata_cloud }}" + name: "{{ volume_image_metadata_volume_name }}" + register: final_volume_info + +- name: Verify image metadata values + assert: + that: + - final_volume_info.volumes[0].volume_image_metadata.disk_format == "qcow2" + - final_volume_info.volumes[0].volume_image_metadata.container_format == "bare" + +# -------------------------------------------------------------------- +# Cleanup +# -------------------------------------------------------------------- +- name: Delete test volume + openstack.cloud.volume: + cloud: "{{ volume_image_metadata_cloud }}" + state: absent + name: "{{ volume_image_metadata_volume_name }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 50e424f3..da2055d7 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -65,3 +65,4 @@ - { role: volume_service, tags: volume_service } - { role: volume_snapshot, tags: volume_snapshot } - { role: volume_type_access, tags: volume_type_access } + - { role: volume_image_metadata, tags: volume_image_metadata } diff --git a/meta/runtime.yml b/meta/runtime.yml index 9356b226..aeb2e1e7 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -97,3 +97,4 @@ action_groups: - volume_snapshot - volume_snapshot_info - volume_type_access + - volume_image_metadata diff --git a/plugins/modules/volume_image_metadata.py b/plugins/modules/volume_image_metadata.py new file mode 100644 index 00000000..1a22bcdb --- /dev/null +++ b/plugins/modules/volume_image_metadata.py @@ -0,0 +1,97 @@ +#!/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_image_metadata +short_description: Manage OpenStack Cinder volume image metadata +extends_documentation_fragment: + - openstack.cloud.openstack +description: + - Set image metadata on a Cinder volume. + - This maps to the Cinder C(os-set_image_metadata) API action. + - This is distinct from regular volume metadata. +options: + volume: + description: + - Volume ID or name. + required: true + type: str + image_metadata: + description: + - Image metadata to apply to the volume. + required: true + type: dict +author: + - Simon Dodsley (@simondodsley) +""" + +EXAMPLES = r""" +- name: Apply volume image metadata + openstack.cloud.volume_image_metadata: + cloud: mycloud + volume: 9c6b7c8d-1234 + image_metadata: + image_id: 2e1a... + disk_format: qcow2 + container_format: bare +""" + +RETURN = r""" +changed: + description: Whether the volume image metadata was changed. + returned: always + type: bool +volume: + description: Volume information. + returned: always + type: dict +""" + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule, +) + + +class VolumeImageMetadataModule(OpenStackModule): + + argument_spec = dict( + volume=dict(required=True), + image_metadata=dict(type="dict", required=True), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def run(self): + volume_ref = self.params["volume"] + desired_meta = self.params["image_metadata"] + + # Resolve volume + volume = self.conn.block_storage.find_volume(volume_ref, ignore_missing=False) + + current_meta = volume.volume_image_metadata or {} + + # Idempotency check + if desired_meta.items() <= current_meta.items(): + self.exit_json(changed=False, volume=volume.to_dict()) + + if not self.ansible.check_mode: + self.conn.block_storage.set_volume_image_metadata(volume.id, **desired_meta) + + volume = self.conn.block_storage.get_volume(volume.id) + + self.exit_json(changed=True, volume=volume.to_dict()) + + +def main(): + module = VolumeImageMetadataModule() + module() + + +if __name__ == "__main__": + main() diff --git a/releasenotes/notes/volume-image-metadata-module-9721e8a08c1cba7c.yaml b/releasenotes/notes/volume-image-metadata-module-9721e8a08c1cba7c.yaml new file mode 100644 index 00000000..92ea2dc5 --- /dev/null +++ b/releasenotes/notes/volume-image-metadata-module-9721e8a08c1cba7c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added a new ``openstack.cloud.volume_image_metadata`` module to manage + Cinder volume image metadata via the ``os-set_image_metadata`` API. + This enables correct preservation of image provenance and boot semantics + for volumes, which cannot be achieved using regular volume metadata.