From 60161ea5b7a7071b790bd7800bd27ddba2eb0dc8 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 23 Dec 2025 09:21:50 -0500 Subject: [PATCH 01/14] Added module for Image Share Group --- .../doc_fragments/image_share_group.py | 34 ++ plugins/modules/image_share_group.py | 363 ++++++++++++++++++ requirements.txt | 3 +- .../targets/image_basic/tasks/main.yaml | 4 + .../image_share_group_basic/tasks/main.yaml | 229 +++++++++++ 5 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 plugins/module_utils/doc_fragments/image_share_group.py create mode 100644 plugins/modules/image_share_group.py create mode 100644 tests/integration/targets/image_share_group_basic/tasks/main.yaml diff --git a/plugins/module_utils/doc_fragments/image_share_group.py b/plugins/module_utils/doc_fragments/image_share_group.py new file mode 100644 index 00000000..12362764 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group.py @@ -0,0 +1,34 @@ +"""Documentation fragments for the Image Share Group module""" +specdoc_examples = [''' +- name: Create a basic image share group + linode.cloud.image_share_group_basic: + label: my-sharegroup + description: "My image share group." + state: present''', ''' +- name: Create a basic image share group with an image + linode.cloud.image_share_group_basic: + label: my-sharegroup + description: "My image share group." + images: + - id: private/123 + label: "My shared image" + description: "An image shared in the group." + state: present + ''', ''' +- name: Delete an image share group + linode.cloud.image_share_group_basic: + label: my-sharegroup + state: absent'''] + +result_image_share_group_samples = ['''{ + "created": "2025-04-14T22:44:02", + "description": "My image share group.", + "expiry": null, + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "my-sharegroup", + "members_count": 0, + "updated": null, + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" +}'''] diff --git a/plugins/modules/image_share_group.py b/plugins/modules/image_share_group.py new file mode 100644 index 00000000..69c17a15 --- /dev/null +++ b/plugins/modules/image_share_group.py @@ -0,0 +1,363 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains all the functionality for Image Share Groups for a Producer.""" + +from __future__ import absolute_import, division, print_function + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.image_share_group as docs +import copy +from typing import Any, Optional +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + filter_null_values_recursive, + handle_updates, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common import ( + LinodeModuleBase, +) + +from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import ( + global_authors, + global_requirements, +) + +from ansible_specdoc.objects import ( + FieldType, + SpecDocMeta, + SpecField, + SpecReturnValue, +) +from linode_api4 import ImageShareGroup, ImageShareGroupImageToAdd, ImageShareGroupImagesToAdd, \ + ImageShareGroupImageToUpdate + +image_share_group_images_spec = { + "id": SpecField( + type=FieldType.string, + required=True, + description=[ + "The id of the Private Image to include in an Image Share Group." + ], + ), + "label": SpecField( + type=FieldType.string, + description=[ + "A label to assign to the Image within the context of an Image Share Group." + ], + ), + "description": SpecField( + type=FieldType.string, + description=[ + "A description to assign to the Image within the context of an Image Share Group." + ], + ), +} + +SPEC = { + "label": SpecField( + type=FieldType.string, + description=["This Image Share Group's unique label."], + ), + "description": SpecField( + type=FieldType.string, + description=["A description of this Image Share Group."], + ), + "images": SpecField( + type=FieldType.list, + element_type=FieldType.dict, + suboptions=image_share_group_images_spec, + description=["A list of images to include in this Image Share Group."], + ), + "state": SpecField( + type=FieldType.string, + description=["The desired state of the target."], + choices=["present", "absent"], + required=True, + ), +} + +SPECDOC_META = SpecDocMeta( + description=["Manage an Image Share Group."], + requirements=global_requirements, + author=global_authors, + options=SPEC, + examples=docs.specdoc_examples, + return_values={ + "image_share_group": SpecReturnValue( + description="The Image Share Group in JSON serialized form.", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup", + type=FieldType.dict, + sample=docs.result_image_share_group_samples, + ) + }, +) + +MUTABLE_FIELDS = {"description"} + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + + +class Module(LinodeModuleBase): + """Module for creating and destroying Image Share Groups""" + + def __init__(self) -> None: + self.module_arg_spec = SPECDOC_META.ansible_spec + self.results = { + "changed": False, + "actions": [], + "image_share_group": None, + } + + super().__init__(module_arg_spec=self.module_arg_spec) + + def _get_image_share_group_by_label(self, label: str) -> Optional[ImageShareGroup]: + try: + return self.client.sharegroups(ImageShareGroup.label == label)[0] + except IndexError: + return None + except Exception as exception: + return self.fail( + msg="failed to get image share group {0}: {1}".format(label, exception) + ) + + def _create(self) -> Optional[ImageShareGroup]: + params = filter_null_values_recursive( + { + "label": self.module.params.get("label"), + "description": self.module.params.get("description"), + "images": self.module.params.get("images"), + } + ) + + try: + return self.client.sharegroups.create_sharegroup(**params) + except Exception as exception: + return self.fail( + msg="failed to create image Share Group: {0}".format(exception) + ) + + def _update(self, image_share_group: ImageShareGroup) -> Optional[ImageShareGroup]: + image_share_group._api_get() + + new_params = filter_null_values_recursive(copy.deepcopy(self.module.params)) + new_params = {k: v for k, v in new_params.items() if k in MUTABLE_FIELDS} + + images_to_update = self.module.params.get("images") + + handle_updates( + image_share_group, new_params, MUTABLE_FIELDS, self.register_action + ) + + # Only touch images if explicitly provided + if images_to_update is not None: + self._update_images(image_share_group, images_to_update) + + return image_share_group + + def _update_images(self, image_share_group: ImageShareGroup, desired_images: list) -> None: + desired_images = desired_images or [] + + # Build a map of the desired state of images by their private IDs + plan_map = { + img["id"]: img + for img in desired_images + } + + # Build a map of the remote state of images by their private IDs + remote_map = {} + for img in image_share_group.get_image_shares(): + shared_by = getattr( + getattr(img, "image_sharing", None), + "shared_by", + None, + ) + private_id = getattr(shared_by, "source_image_id", None) + if not private_id: + continue + + remote_map[private_id] = { + "shared_id": getattr(img, "id", None), + "label": getattr(img, "label", None), + "description": getattr(img, "description", None), + } + + to_add = [] + to_update = [] + to_remove = [] + + # Determine which images to add and update + for private_id, desired_image in plan_map.items(): + remote = remote_map.get(private_id) + + if not remote: + # Not present remotely, should be added + to_add.append({ + "id": private_id, + "label": desired_image.get("label"), + "description": desired_image.get("description"), + }) + continue + + # Present remotely, check if an update is needed + label_changed = desired_image.get("label") != remote.get("label") + desc_changed = desired_image.get("description") != remote.get("description") + + if label_changed or desc_changed: + opts = {} + if label_changed: + opts["label"] = desired_image.get("label") + if desc_changed: + opts["description"] = desired_image.get("description") + + to_update.append({ + "shared_id": remote["shared_id"], + "opts": opts, + }) + + # Determine which images to remove + for private_id, remote in remote_map.items(): + if private_id not in plan_map: + to_remove.append(remote["shared_id"]) + + # Add + if to_add: + try: + images_to_add = ImageShareGroupImagesToAdd( + images=[ + ImageShareGroupImageToAdd( + id=img["id"], + label=img.get("label"), + description=img.get("description"), + ) + for img in to_add + ] + ) + + image_share_group.add_images(images_to_add) + + for img in to_add: + self.register_action(f"Added image {img['id']}") + + self.results["changed"] = True + + except Exception as e: + self.fail(msg=f"Failed to add images: {e}") + + # Update + for update in to_update: + try: + kwargs = {} + if 'label' in update['opts']: + kwargs['label'] = update['opts']['label'] + if 'description' in update['opts']: + kwargs['description'] = update['opts']['description'] + + images_to_update = ImageShareGroupImageToUpdate( + image_share_id=update['shared_id'], + **kwargs + ) + + image_share_group.update_image_share(images_to_update) + self.register_action(f"Updated image {update['shared_id']}") + self.results["changed"] = True + except Exception as e: + self.fail(msg=f"Failed to update image {update['shared_id']}: {e}") + + # Remove + for img_shared_id in to_remove: + try: + image_share_group.revoke_image_share(img_shared_id) + self.register_action(f"Removed image {img_shared_id}") + self.results["changed"] = True + except Exception as e: + self.fail(msg=f"Failed to remove image {img_shared_id}: {e}") + + def _populate_results(self, sharegroup: ImageShareGroup) -> None: + sharegroup._api_get() + raw = sharegroup._raw_json + + images_param = self.module.params.get("images") + + if images_param is not None: + # User explicitly provided images + raw["images"] = images_param + else: + # User omitted images so fetch current images from API + images = [] + for img in sharegroup.get_image_shares(): + shared_by = getattr(getattr(img, "image_sharing", None), "shared_by", None) + private_id = getattr(shared_by, "source_image_id", None) + if private_id: + images.append({ + "id": private_id, + "label": getattr(img, "label", None), + "description": getattr(img, "description", None), + }) + raw["images"] = images + + self.results["image_share_group"] = raw + + # def _populate_results(self, sharegroup: ImageShareGroup) -> None: + # sharegroup._api_get() + # raw = sharegroup._raw_json + # + # # If images were explicitly provided, they define the new state + # if self.module.params.get("images") is not None: + # raw["images"] = self.module.params.get("images") + # + # # Otherwise, preserve the previously known Ansible state + # else: + # previous = self.results.get("image_share_group") or {} + # raw["images"] = previous.get("images", []) + # + # self.results["image_share_group"] = raw + + def _handle_present(self) -> None: + label = self.module.params.get("label") + image_share_group = self._get_image_share_group_by_label( + self.module.params.get("label") + ) + + if not image_share_group: + if not label: + self.fail(msg="`label` is required when creating a new Image Share Group") + image_share_group = self._create() + + self._update(image_share_group) + + self._populate_results(image_share_group) + + def _handle_absent(self) -> None: + label: str = self.module.params.get("label") + + sharegroup = self._get_image_share_group_by_label(label) + + if sharegroup is not None: + self.results["image_share_group"] = sharegroup._raw_json + sharegroup.delete() + self.register_action("Deleted image share group {0}".format(label)) + + def exec_module(self, **kwargs: Any) -> Optional[dict]: + """Entrypoint for Image Share Group module""" + state = kwargs.get("state") + + if state == "absent": + self._handle_absent() + return self.results + + self._handle_present() + + return self.results + + +def main() -> None: + """Constructs and calls the Image Share Group module""" + Module() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 85c35e1a..8182a627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -linode-api4>=5.38.0 +#linode-api4>=5.38.0 +git+https://github.com/linode/linode_api4-python.git@ce04f894279cd01ad1d2bc82b223cec4cadb5253#egg=linode-api4 polling==0.3.2 ansible-specdoc>=0.0.19 diff --git a/tests/integration/targets/image_basic/tasks/main.yaml b/tests/integration/targets/image_basic/tasks/main.yaml index 69e6ea57..4f501215 100644 --- a/tests/integration/targets/image_basic/tasks/main.yaml +++ b/tests/integration/targets/image_basic/tasks/main.yaml @@ -41,6 +41,8 @@ - image_create.image.capabilities[0] == 'cloud-init' - image_create.image.tags | length == 2 - image_create.image.size == image_create.image.total_size + - image_create.image.is_shared == false + - image_create.image.image_sharing.shared_with.sharegroup_count == 0 - name: Get image_info by ID linode.cloud.image_info: @@ -53,6 +55,8 @@ - info_id.image.status == 'available' - info_id.image.description == 'cool' - info_id.image.id == image_create.image.id + - info_id.image.is_shared == false + - info_id.image.image_sharing.shared_with.sharegroup_count == 0 - name: Get image_info by label linode.cloud.image_info: diff --git a/tests/integration/targets/image_share_group_basic/tasks/main.yaml b/tests/integration/targets/image_share_group_basic/tasks/main.yaml new file mode 100644 index 00000000..da0b2779 --- /dev/null +++ b/tests/integration/targets/image_share_group_basic/tasks/main.yaml @@ -0,0 +1,229 @@ +- name: image_share_group_basic + block: + - set_fact: + r: "{{ 1000000000 | random }}" + disallowed_image_regions: ["gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3", "no-osl-1"] + + - name: List regions + linode.cloud.region_list: { } + register: all_regions + + - set_fact: + capable_regions: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Object Storage") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' + + - name: Create instance one to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-one' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_one_create + + - name: Create image one + linode.cloud.image: + disk_id: "{{ instance_one_create.disks.0.id }}" + label: image-one + description: first image + state: present + register: image_one_create + + - name: Create instance two to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-two' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_two_create + + - name: Create image two + linode.cloud.image: + disk_id: "{{ instance_two_create.disks.0.id }}" + label: image-two + description: second-image + state: present + register: image_two_create + + - name: Assert images are created + assert: + that: + - image_one_create.image.status == 'available' + - image_two_create.image.status == 'available' + - image_one_create.image.image_sharing.shared_with.sharegroup_count == 0 + - image_two_create.image.image_sharing.shared_with.sharegroup_count == 0 + + - name: Create an image share group without any images + linode.cloud.image_share_group: + label: 'ansible-share-group-empty' + description: 'share group desc' + state: present + register: share_group_empty_create + + - name: Assert image share group is created + assert: + that: + - share_group_empty_create.image_share_group.label == 'ansible-share-group-empty' + - share_group_empty_create.image_share_group.description == 'share group desc' + - share_group_empty_create.image_share_group.images_count == 0 + + - name: Add images to share group + linode.cloud.image_share_group: + label: '{{ share_group_empty_create.image_share_group.label }}' + images: + - id: '{{ image_one_create.image.id }}' + label: 'image-one' + description: 'first image' + - id: '{{ image_two_create.image.id }}' + label: 'image-two' + state: present + register: share_group_empty_add_images + + - name: Select images for assertions + set_fact: + img_one: "{{ share_group_empty_add_images.image_share_group.images | selectattr('id', 'equalto', image_one_create.image.id) | first }}" + img_two: "{{ share_group_empty_add_images.image_share_group.images | selectattr('id', 'equalto', image_two_create.image.id) | first }}" + + - name: Assert images added to share group + assert: + that: + - img_one.label == 'image-one' + - img_one.description == 'first image' + - img_two.label == 'image-two' + - img_two.description is none + - share_group_empty_add_images.image_share_group.images | length == 2 + - share_group_empty_add_images.image_share_group.images_count == 2 + + - name: Update images in share group + linode.cloud.image_share_group: + label: '{{ share_group_empty_add_images.image_share_group.label }}' + images: + - id: '{{ image_one_create.image.id }}' + label: 'image-one-updated' + description: 'first image updated' + state: present + register: share_group_empty_update_images + + - name: Select updated images for assertions + set_fact: + img_one_updated: "{{ share_group_empty_update_images.image_share_group.images | selectattr('id', 'equalto', image_one_create.image.id) | first }}" + img_two_removed: "{{ share_group_empty_update_images.image_share_group.images | selectattr('id', 'equalto', image_two_create.image.id) | list }}" + + - name: Assert images updated in share group + assert: + that: + - img_one_updated.label == 'image-one-updated' + - img_one_updated.description == 'first image updated' + - img_two_removed | length == 0 + - share_group_empty_update_images.image_share_group.images | length == 1 + - share_group_empty_update_images.image_share_group.images_count == 1 + + - name: Remove remaining images in share group + linode.cloud.image_share_group: + label: '{{ share_group_empty_update_images.image_share_group.label }}' + images: [] + state: present + register: share_group_empty_images_removed + + - name: Select removed images for assertions + set_fact: + img_one_removed: "{{ share_group_empty_images_removed.image_share_group.images | selectattr('id', 'equalto', image_one_create.image.id) | list }}" + img_two_removed: "{{ share_group_empty_images_removed.image_share_group.images | selectattr('id', 'equalto', image_two_create.image.id) | list }}" + + - name: Assert images removed from share group + assert: + that: + - img_one_removed | length == 0 + - img_two_removed | length == 0 + - share_group_empty_images_removed.image_share_group.images | length == 0 + - share_group_empty_images_removed.image_share_group.images_count == 0 + + - name: Update image share group details + linode.cloud.image_share_group: + label: '{{ share_group_empty_images_removed.image_share_group.label }}' + description: 'share group desc updated' + state: present + register: share_group_update_details + + - name: Assert image share group details updated + assert: + that: + - share_group_update_details.image_share_group.description == 'share group desc updated' + + - name: Create an image share group with an image + linode.cloud.image_share_group: + label: 'ansible-share-group-populated' + images: + - id: '{{ image_one_create.image.id }}' + label: 'image-one' + description: 'first image' + state: present + register: share_group_populated_create + + - name: Assert image share group is created + assert: + that: + - share_group_populated_create.image_share_group.label == 'ansible-share-group-populated' + - share_group_populated_create.image_share_group.images_count == 1 + + - name: Check that removing images field leaves images unchanged + linode.cloud.image_share_group: + label: '{{ share_group_populated_create.image_share_group.label }}' + state: present + register: share_group_populated_no_change + + - name: Assert images unchanged in share group + assert: + that: + - share_group_populated_no_change.image_share_group.images | length == 1 + - share_group_populated_no_change.image_share_group.images_count == 1 + + - name: Remove remaining images in share group + linode.cloud.image_share_group: + label: '{{ share_group_populated_create.image_share_group.label }}' + images: [] + state: present + register: share_group_populated_images_removed + + - name: Assert images removed from share group + assert: + that: + - share_group_populated_images_removed.image_share_group.images | length == 0 + - share_group_populated_images_removed.image_share_group.images_count == 0 + + always: + - ignore_errors: yes + block: + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_update_details.image_share_group.label }}' + state: absent + + - name: Delete image + linode.cloud.image: + label: '{{ image_one_create.image.label }}' + state: absent + + - name: Delete image + linode.cloud.image: + label: '{{ image_two_create.image.label }}' + state: absent + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_one_create.instance.label }}' + state: absent + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_two_create.instance.label }}' + state: absent + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' \ No newline at end of file From bdbcebf5a6d524265b01832c501091a47086a898 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 23 Dec 2025 14:37:23 -0500 Subject: [PATCH 02/14] Added module for Image Share Group Token --- Makefile | 5 + .../doc_fragments/image_share_group.py | 14 +- .../doc_fragments/image_share_group_token.py | 27 +++ plugins/modules/image_share_group.py | 23 +-- plugins/modules/image_share_group_token.py | 161 ++++++++++++++++++ .../image_share_group_token/tasks/main.yaml | 77 +++++++++ 6 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 plugins/module_utils/doc_fragments/image_share_group_token.py create mode 100644 plugins/modules/image_share_group_token.py create mode 100644 tests/integration/targets/image_share_group_token/tasks/main.yaml diff --git a/Makefile b/Makefile index 9b80db77..8515b6aa 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,11 @@ else echo "LINODE_API_TOKEN must be set"; \ exit 1; endif + # Add producer/consumer tokens, may be empty + @echo "producer_api_token: $${LINODE_PRODUCER_API_TOKEN:-}" >> $(INTEGRATION_CONFIG) + @echo "consumer_api_token: $${LINODE_CONSUMER_API_TOKEN:-}" >> $(INTEGRATION_CONFIG) + + # Common settings @echo "ua_prefix: E2E" >> $(INTEGRATION_CONFIG) @echo "api_url: $$(url=$${LINODE_API_URL:-$${TEST_API_URL:-https://api.linode.com}}; echo $${url%/}/)" >> $(INTEGRATION_CONFIG) @echo "api_version: $${LINODE_API_VERSION:-$${TEST_API_VERSION:-v4beta}}" >> $(INTEGRATION_CONFIG) diff --git a/plugins/module_utils/doc_fragments/image_share_group.py b/plugins/module_utils/doc_fragments/image_share_group.py index 12362764..b3770375 100644 --- a/plugins/module_utils/doc_fragments/image_share_group.py +++ b/plugins/module_utils/doc_fragments/image_share_group.py @@ -1,23 +1,23 @@ """Documentation fragments for the Image Share Group module""" specdoc_examples = [''' - name: Create a basic image share group - linode.cloud.image_share_group_basic: - label: my-sharegroup + linode.cloud.image_share_group: + label: "my-sharegroup" description: "My image share group." state: present''', ''' - name: Create a basic image share group with an image - linode.cloud.image_share_group_basic: - label: my-sharegroup + linode.cloud.image_share_group: + label: "my-sharegroup" description: "My image share group." images: - - id: private/123 + - id: "private/123" label: "My shared image" description: "An image shared in the group." state: present ''', ''' - name: Delete an image share group - linode.cloud.image_share_group_basic: - label: my-sharegroup + linode.cloud.image_share_group: + label: "my-sharegroup" state: absent'''] result_image_share_group_samples = ['''{ diff --git a/plugins/module_utils/doc_fragments/image_share_group_token.py b/plugins/module_utils/doc_fragments/image_share_group_token.py new file mode 100644 index 00000000..cc6dc9a1 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_token.py @@ -0,0 +1,27 @@ +"""Documentation fragments for the Image Share Group Token module""" +specdoc_examples = [''' +- name: Create an image share group token + linode.cloud.image_share_group_token: + label: "my-sharegroup-token" + valid_for_sharegroup_uuid: "1533863e-16a4-47b5-b829-ac0f35c13278" + state: present''', ''' +- name: Delete an image share group + linode.cloud.image_share_group_token: + valid_for_sharegroup_uuid: "e1d0e58b-f89f-4237-84ab-b82077342359" + state: absent'''] + +result_image_share_group_token_samples = ['''{ + "created": "2025-08-04T10:09:09", + "expiry": null, + "label": "Backend Services - Engineering", + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": null, + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" +}'''] + +result_single_use_token_samples = ['''{ + "token": "abcdefghijklmnopqrstuvwxyz1234567890" +}'''] \ No newline at end of file diff --git a/plugins/modules/image_share_group.py b/plugins/modules/image_share_group.py index 69c17a15..a4be11a4 100644 --- a/plugins/modules/image_share_group.py +++ b/plugins/modules/image_share_group.py @@ -56,6 +56,7 @@ "label": SpecField( type=FieldType.string, description=["This Image Share Group's unique label."], + required=True, ), "description": SpecField( type=FieldType.string, @@ -137,7 +138,7 @@ def _create(self) -> Optional[ImageShareGroup]: return self.client.sharegroups.create_sharegroup(**params) except Exception as exception: return self.fail( - msg="failed to create image Share Group: {0}".format(exception) + msg="failed to create Image Share Group: {0}".format(exception) ) def _update(self, image_share_group: ImageShareGroup) -> Optional[ImageShareGroup]: @@ -301,31 +302,15 @@ def _populate_results(self, sharegroup: ImageShareGroup) -> None: self.results["image_share_group"] = raw - # def _populate_results(self, sharegroup: ImageShareGroup) -> None: - # sharegroup._api_get() - # raw = sharegroup._raw_json - # - # # If images were explicitly provided, they define the new state - # if self.module.params.get("images") is not None: - # raw["images"] = self.module.params.get("images") - # - # # Otherwise, preserve the previously known Ansible state - # else: - # previous = self.results.get("image_share_group") or {} - # raw["images"] = previous.get("images", []) - # - # self.results["image_share_group"] = raw - def _handle_present(self) -> None: label = self.module.params.get("label") image_share_group = self._get_image_share_group_by_label( - self.module.params.get("label") + label ) if not image_share_group: - if not label: - self.fail(msg="`label` is required when creating a new Image Share Group") image_share_group = self._create() + self.register_action("Created Image Share Group {0}".format(image_share_group.label)) self._update(image_share_group) diff --git a/plugins/modules/image_share_group_token.py b/plugins/modules/image_share_group_token.py new file mode 100644 index 00000000..9fb4266a --- /dev/null +++ b/plugins/modules/image_share_group_token.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains all the functionality for Image Share Group Tokens for a Consumer.""" + +from __future__ import absolute_import, division, print_function + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.image_share_group_token as docs +from typing import Optional, Any +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + filter_null_values, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common import ( + LinodeModuleBase, +) + +from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import ( + global_authors, + global_requirements, +) + +from ansible_specdoc.objects import ( + FieldType, + SpecDocMeta, + SpecField, + SpecReturnValue, +) +from linode_api4 import ImageShareGroupToken + +SPEC = { + "label": SpecField( + type=FieldType.string, + description=["This Image Share Group Token's unique label."], + required=True, + ), + "valid_for_sharegroup_uuid": SpecField( + type=FieldType.string, + description=["The UUID of the Image Share Group that this token is valid for."], + required=True, + ), + "state": SpecField( + type=FieldType.string, + description=["The desired state of the target."], + choices=["present", "absent"], + required=True, + ), +} + +SPECDOC_META = SpecDocMeta( + description=["Manage an Image Share Group Token."], + requirements=global_requirements, + author=global_authors, + options=SPEC, + examples=docs.specdoc_examples, + return_values={ + "image_share_group_token": SpecReturnValue( + description="The Image Share Group Token in JSON serialized form.", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-token", + type=FieldType.dict, + sample=docs.result_image_share_group_token_samples, + ), + "single_use_token": SpecReturnValue( + description="The single use token string to provide to a Image Share Group Producer " + "to be added to the share group.", + type=FieldType.string, + sample=docs.result_single_use_token_samples, + ) + }, +) + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + + +class Module(LinodeModuleBase): + """Module for creating and destroying Image Share Group Tokens""" + + def __init__(self) -> None: + self.module_arg_spec = SPECDOC_META.ansible_spec + self.results = { + "changed": False, + "actions": [], + "image_share_group_token": None, + "single_use_token": None, + } + + super().__init__(module_arg_spec=self.module_arg_spec) + + def _get_image_share_group_token_by_label(self, label: str) -> Optional[ImageShareGroupToken]: + try: + return self.client.sharegroups.tokens(ImageShareGroupToken.label == label)[0] + except IndexError: + return None + except Exception as exception: + return self.fail( + msg="failed to get image share group token for label {0}: {1}".format(label, exception) + ) + + def _create(self) -> Optional[ImageShareGroupToken]: + params = filter_null_values( + { + "label": self.module.params.get("label"), + "valid_for_sharegroup_uuid": self.module.params.get("valid_for_sharegroup_uuid"), + } + ) + + try: + token_obj, single_use_token = self.client.sharegroups.create_token(**params) + self.results["single_use_token"] = single_use_token + return token_obj + except Exception as exception: + return self.fail( + msg="failed to create Image Share Group Token: {0}".format(exception) + ) + + def _handle_present(self) -> None: + label = self.module.params.get("label") + token = self._get_image_share_group_token_by_label(label) + + if not token: + token = self._create() + self.register_action("Created Image Share Group Token {0}".format(label)) + + # Force lazy-loading + token._api_get() + + self.results["image_share_group_token"] = token._raw_json + + def _handle_absent(self) -> None: + label: str = self.module.params.get("label") + token = self._get_image_share_group_token_by_label(label) + + if token is not None: + self.results["image_share_group_token"] = token._raw_json + token.delete() + self.register_action("Deleted image share group token {0}".format(label)) + + def exec_module(self, **kwargs: Any) -> Optional[dict]: + """Entrypoint for Image Share Group Token module""" + state = kwargs.get("state") + + if state == "absent": + self._handle_absent() + return self.results + + self._handle_present() + + return self.results + + +def main() -> None: + """Constructs and calls the Image Share Group Token module""" + Module() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/image_share_group_token/tasks/main.yaml b/tests/integration/targets/image_share_group_token/tasks/main.yaml new file mode 100644 index 00000000..1f946c85 --- /dev/null +++ b/tests/integration/targets/image_share_group_token/tasks/main.yaml @@ -0,0 +1,77 @@ +- name: image_share_group_token + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert image share group is created + assert: + that: + - share_group_create.image_share_group.label == "ansible-test-" ~ r + - share_group_create.image_share_group.images_count == 0 + - share_group_create.image_share_group.members_count == 0 + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert image share group token is created + assert: + that: + - share_group_token_create.image_share_group_token.label == "ansible-test-" ~ r + - share_group_token_create.image_share_group_token.valid_for_sharegroup_uuid == share_group_create.image_share_group.uuid + - share_group_token_create.single_use_token is defined + - share_group_token_create.single_use_token | length > 0 + + always: + - ignore_errors: yes + block: + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + valid_for_sharegroup_uuid: "{{ share_group_token_create.image_share_group_token.valid_for_sharegroup_uuid }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file From b7a8b53df526ffabcdb7dc8ca57e11c40e874874 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 30 Dec 2025 15:13:18 -0500 Subject: [PATCH 03/14] Added module for Image Share Group Member --- README.md | 3 + docs/modules/image_share_group.md | 84 ++++++++ docs/modules/image_share_group_member.md | 60 ++++++ docs/modules/image_share_group_token.md | 71 +++++++ .../doc_fragments/image_share_group_member.py | 21 ++ .../doc_fragments/image_share_group_token.py | 6 +- plugins/modules/image_share_group.py | 116 +++++++---- plugins/modules/image_share_group_member.py | 196 ++++++++++++++++++ plugins/modules/image_share_group_token.py | 61 ++++-- .../image_share_group_member/tasks/main.yaml | 102 +++++++++ .../image_share_group_token/tasks/main.yaml | 1 - 11 files changed, 652 insertions(+), 69 deletions(-) create mode 100644 docs/modules/image_share_group.md create mode 100644 docs/modules/image_share_group_member.md create mode 100644 docs/modules/image_share_group_token.md create mode 100644 plugins/module_utils/doc_fragments/image_share_group_member.py create mode 100644 plugins/modules/image_share_group_member.py create mode 100644 tests/integration/targets/image_share_group_member/tasks/main.yaml diff --git a/README.md b/README.md index 9fd273d9..0e8b9506 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ Name | Description | [linode.cloud.firewall_device](./docs/modules/firewall_device.md)|Manage Linode Firewall Devices.| [linode.cloud.firewall_settings](./docs/modules/firewall_settings.md)|Configure the firewall settings for the account.| [linode.cloud.image](./docs/modules/image.md)|Manage a Linode Image.| +[linode.cloud.image_share_group](./docs/modules/image_share_group.md)|Manage an Image Share Group.| +[linode.cloud.image_share_group_member](./docs/modules/image_share_group_member.md)|Manage an Image Share Group Member.| +[linode.cloud.image_share_group_token](./docs/modules/image_share_group_token.md)|Manage an Image Share Group Token.| [linode.cloud.instance](./docs/modules/instance.md)|Manage Linode Instances, Configs, and Disks.| [linode.cloud.instance_interface_settings](./docs/modules/instance_interface_settings.md)|Create, read, and update the interface settings for a Linode instance.| [linode.cloud.ip](./docs/modules/ip.md)|Allocates a new IPv4 Address on your Account. The Linode must be configured to support additional addresses - please Open a support ticket requesting additional addresses before attempting allocation.| diff --git a/docs/modules/image_share_group.md b/docs/modules/image_share_group.md new file mode 100644 index 00000000..3ffc598d --- /dev/null +++ b/docs/modules/image_share_group.md @@ -0,0 +1,84 @@ +# image_share_group + +Manage an Image Share Group. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Create a basic image share group + linode.cloud.image_share_group: + label: "my-sharegroup" + description: "My image share group." + state: present +``` + +```yaml +- name: Create a basic image share group with an image + linode.cloud.image_share_group: + label: "my-sharegroup" + description: "My image share group." + images: + - id: "private/123" + label: "My shared image" + description: "An image shared in the group." + state: present + +``` + +```yaml +- name: Delete an image share group + linode.cloud.image_share_group: + label: "my-sharegroup" + state: absent +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `label` |
`str`
|
**Required**
| This Image Share Group's unique label. | +| `state` |
`str`
|
**Required**
| The desired state of the target. **(Choices: `present`, `absent`)** | +| `description` |
`str`
|
Optional
| A description of this Image Share Group. | +| [`images` (sub-options)](#images) |
`list`
|
Optional
| A list of images to include in this Image Share Group. | + +### images + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `id` |
`str`
|
**Required**
| The id of the Private Image to include in an Image Share Group. | +| `label` |
`str`
|
Optional
| A label to assign to the Image within the context of an Image Share Group. | +| `description` |
`str`
|
Optional
| A description to assign to the Image within the context of an Image Share Group. | + +## Return Values + +- `image_share_group` - The Image Share Group in JSON serialized form. + + - Sample Response: + ```json + { + "created": "2025-04-14T22:44:02", + "description": "My image share group.", + "expiry": null, + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "my-sharegroup", + "members_count": 0, + "updated": null, + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup) for a list of returned fields + + diff --git a/docs/modules/image_share_group_member.md b/docs/modules/image_share_group_member.md new file mode 100644 index 00000000..0ae7287c --- /dev/null +++ b/docs/modules/image_share_group_member.md @@ -0,0 +1,60 @@ +# image_share_group_member + +Manage an Image Share Group Member. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Create an image share group member + linode.cloud.image_share_group_member: + label: "my-sharegroup-member" + token: "abcdefghijklmnopqrstuvwxyz1234567890" + state: present +``` + +```yaml +- name: Delete an image share group member + linode.cloud.image_share_group_member: + label: "my-sharegroup-member" + state: absent +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `label` |
`str`
|
**Required**
| This Image Share Group Member's unique label. | +| `sharegroup_id` |
`int`
|
**Required**
| The ID of the Image Share Group this member belongs to. | +| `state` |
`str`
|
**Required**
| The desired state of the target. **(Choices: `present`, `absent`)** | +| `token` |
`str`
|
Optional
| A single-use Image Share Group Token provided by the Consumer. This value is required when creating a member and is never returned. | + +## Return Values + +- `image_share_group_member` - The Image Share Group Member in JSON serialized form. + + - Sample Response: + ```json + { + "token_uuid": "24wef-243qg-45wgg-q343q", + "status": "active", + "label": "my-sharegroup-member", + "created": "2016-03-16T17:30:49", + "updated": "2016-03-18T17:30:49", + "expiry": "2016-03-18T17:30:49" + } + + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-member-token) for a list of returned fields + + diff --git a/docs/modules/image_share_group_token.md b/docs/modules/image_share_group_token.md new file mode 100644 index 00000000..2c9ca49d --- /dev/null +++ b/docs/modules/image_share_group_token.md @@ -0,0 +1,71 @@ +# image_share_group_token + +Manage an Image Share Group Token. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Create an image share group token + linode.cloud.image_share_group_token: + label: "my-sharegroup-token" + valid_for_sharegroup_uuid: "1533863e-16a4-47b5-b829-ac0f35c13278" + state: present +``` + +```yaml +- name: Delete an image share group token + linode.cloud.image_share_group_token: + label: "my-sharegroup-token" + state: absent +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `label` |
`str`
|
**Required**
| This Image Share Group Token's unique label. | +| `state` |
`str`
|
**Required**
| The desired state of the target. **(Choices: `present`, `absent`)** | +| `valid_for_sharegroup_uuid` |
`str`
|
Optional
| The UUID of the Image Share Group that this token is valid for. | + +## Return Values + +- `image_share_group_token` - The Image Share Group Token in JSON serialized form. + + - Sample Response: + ```json + { + "created": "2025-08-04T10:09:09", + "expiry": null, + "label": "Backend Services - Engineering", + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": null, + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-token) for a list of returned fields + + +- `single_use_token` - The single use token string to provide to a Image Share Group Producer to be added to the share group. + + - Sample Response: + ```json + { + "token": "abcdefghijklmnopqrstuvwxyz1234567890" + } + ``` + + diff --git a/plugins/module_utils/doc_fragments/image_share_group_member.py b/plugins/module_utils/doc_fragments/image_share_group_member.py new file mode 100644 index 00000000..8810793d --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_member.py @@ -0,0 +1,21 @@ +"""Documentation fragments for the Image Share Group Member module""" +specdoc_examples = [''' +- name: Create an image share group member + linode.cloud.image_share_group_member: + label: "my-sharegroup-member" + token: "abcdefghijklmnopqrstuvwxyz1234567890" + state: present''', ''' +- name: Delete an image share group member + linode.cloud.image_share_group_member: + label: "my-sharegroup-member" + state: absent'''] + +result_image_share_group_member_samples = ['''{ + "token_uuid": "24wef-243qg-45wgg-q343q", + "status": "active", + "label": "my-sharegroup-member", + "created": "2016-03-16T17:30:49", + "updated": "2016-03-18T17:30:49", + "expiry": "2016-03-18T17:30:49" +} +'''] diff --git a/plugins/module_utils/doc_fragments/image_share_group_token.py b/plugins/module_utils/doc_fragments/image_share_group_token.py index cc6dc9a1..ddd0ba9d 100644 --- a/plugins/module_utils/doc_fragments/image_share_group_token.py +++ b/plugins/module_utils/doc_fragments/image_share_group_token.py @@ -5,9 +5,9 @@ label: "my-sharegroup-token" valid_for_sharegroup_uuid: "1533863e-16a4-47b5-b829-ac0f35c13278" state: present''', ''' -- name: Delete an image share group +- name: Delete an image share group token linode.cloud.image_share_group_token: - valid_for_sharegroup_uuid: "e1d0e58b-f89f-4237-84ab-b82077342359" + label: "my-sharegroup-token" state: absent'''] result_image_share_group_token_samples = ['''{ @@ -24,4 +24,4 @@ result_single_use_token_samples = ['''{ "token": "abcdefghijklmnopqrstuvwxyz1234567890" -}'''] \ No newline at end of file +}'''] diff --git a/plugins/modules/image_share_group.py b/plugins/modules/image_share_group.py index a4be11a4..80d51661 100644 --- a/plugins/modules/image_share_group.py +++ b/plugins/modules/image_share_group.py @@ -5,30 +5,33 @@ from __future__ import absolute_import, division, print_function -import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.image_share_group as docs import copy from typing import Any, Optional -from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( - filter_null_values_recursive, - handle_updates, -) + +import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.image_share_group as docs from ansible_collections.linode.cloud.plugins.module_utils.linode_common import ( LinodeModuleBase, ) - from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import ( global_authors, global_requirements, ) - +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + filter_null_values_recursive, + handle_updates, +) from ansible_specdoc.objects import ( FieldType, SpecDocMeta, SpecField, SpecReturnValue, ) -from linode_api4 import ImageShareGroup, ImageShareGroupImageToAdd, ImageShareGroupImagesToAdd, \ - ImageShareGroupImageToUpdate +from linode_api4 import ( + ImageShareGroup, + ImageShareGroupImagesToAdd, + ImageShareGroupImageToAdd, + ImageShareGroupImageToUpdate, +) image_share_group_images_spec = { "id": SpecField( @@ -115,14 +118,18 @@ def __init__(self) -> None: super().__init__(module_arg_spec=self.module_arg_spec) - def _get_image_share_group_by_label(self, label: str) -> Optional[ImageShareGroup]: + def _get_image_share_group_by_label( + self, label: str + ) -> Optional[ImageShareGroup]: try: return self.client.sharegroups(ImageShareGroup.label == label)[0] except IndexError: return None except Exception as exception: return self.fail( - msg="failed to get image share group {0}: {1}".format(label, exception) + msg="failed to get image share group {0}: {1}".format( + label, exception + ) ) def _create(self) -> Optional[ImageShareGroup]: @@ -141,11 +148,17 @@ def _create(self) -> Optional[ImageShareGroup]: msg="failed to create Image Share Group: {0}".format(exception) ) - def _update(self, image_share_group: ImageShareGroup) -> Optional[ImageShareGroup]: + def _update( + self, image_share_group: ImageShareGroup + ) -> Optional[ImageShareGroup]: image_share_group._api_get() - new_params = filter_null_values_recursive(copy.deepcopy(self.module.params)) - new_params = {k: v for k, v in new_params.items() if k in MUTABLE_FIELDS} + new_params = filter_null_values_recursive( + copy.deepcopy(self.module.params) + ) + new_params = { + k: v for k, v in new_params.items() if k in MUTABLE_FIELDS + } images_to_update = self.module.params.get("images") @@ -159,14 +172,14 @@ def _update(self, image_share_group: ImageShareGroup) -> Optional[ImageShareGrou return image_share_group - def _update_images(self, image_share_group: ImageShareGroup, desired_images: list) -> None: + # pylint: disable=too-many-statements + def _update_images( + self, image_share_group: ImageShareGroup, desired_images: list + ) -> None: desired_images = desired_images or [] # Build a map of the desired state of images by their private IDs - plan_map = { - img["id"]: img - for img in desired_images - } + plan_map = {img["id"]: img for img in desired_images} # Build a map of the remote state of images by their private IDs remote_map = {} @@ -196,16 +209,20 @@ def _update_images(self, image_share_group: ImageShareGroup, desired_images: lis if not remote: # Not present remotely, should be added - to_add.append({ - "id": private_id, - "label": desired_image.get("label"), - "description": desired_image.get("description"), - }) + to_add.append( + { + "id": private_id, + "label": desired_image.get("label"), + "description": desired_image.get("description"), + } + ) continue # Present remotely, check if an update is needed label_changed = desired_image.get("label") != remote.get("label") - desc_changed = desired_image.get("description") != remote.get("description") + desc_changed = desired_image.get("description") != remote.get( + "description" + ) if label_changed or desc_changed: opts = {} @@ -214,10 +231,12 @@ def _update_images(self, image_share_group: ImageShareGroup, desired_images: lis if desc_changed: opts["description"] = desired_image.get("description") - to_update.append({ - "shared_id": remote["shared_id"], - "opts": opts, - }) + to_update.append( + { + "shared_id": remote["shared_id"], + "opts": opts, + } + ) # Determine which images to remove for private_id, remote in remote_map.items(): @@ -252,21 +271,22 @@ def _update_images(self, image_share_group: ImageShareGroup, desired_images: lis for update in to_update: try: kwargs = {} - if 'label' in update['opts']: - kwargs['label'] = update['opts']['label'] - if 'description' in update['opts']: - kwargs['description'] = update['opts']['description'] + if "label" in update["opts"]: + kwargs["label"] = update["opts"]["label"] + if "description" in update["opts"]: + kwargs["description"] = update["opts"]["description"] images_to_update = ImageShareGroupImageToUpdate( - image_share_id=update['shared_id'], - **kwargs + image_share_id=update["shared_id"], **kwargs ) image_share_group.update_image_share(images_to_update) self.register_action(f"Updated image {update['shared_id']}") self.results["changed"] = True except Exception as e: - self.fail(msg=f"Failed to update image {update['shared_id']}: {e}") + self.fail( + msg=f"Failed to update image {update['shared_id']}: {e}" + ) # Remove for img_shared_id in to_remove: @@ -290,27 +310,31 @@ def _populate_results(self, sharegroup: ImageShareGroup) -> None: # User omitted images so fetch current images from API images = [] for img in sharegroup.get_image_shares(): - shared_by = getattr(getattr(img, "image_sharing", None), "shared_by", None) + shared_by = getattr( + getattr(img, "image_sharing", None), "shared_by", None + ) private_id = getattr(shared_by, "source_image_id", None) if private_id: - images.append({ - "id": private_id, - "label": getattr(img, "label", None), - "description": getattr(img, "description", None), - }) + images.append( + { + "id": private_id, + "label": getattr(img, "label", None), + "description": getattr(img, "description", None), + } + ) raw["images"] = images self.results["image_share_group"] = raw def _handle_present(self) -> None: label = self.module.params.get("label") - image_share_group = self._get_image_share_group_by_label( - label - ) + image_share_group = self._get_image_share_group_by_label(label) if not image_share_group: image_share_group = self._create() - self.register_action("Created Image Share Group {0}".format(image_share_group.label)) + self.register_action( + "Created Image Share Group {0}".format(image_share_group.label) + ) self._update(image_share_group) diff --git a/plugins/modules/image_share_group_member.py b/plugins/modules/image_share_group_member.py new file mode 100644 index 00000000..ec83cb41 --- /dev/null +++ b/plugins/modules/image_share_group_member.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains all the functionality for Image Share Group Members for a Producer.""" + +from __future__ import absolute_import, division, print_function + +from typing import Any, Dict, Optional + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_member as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common import ( + LinodeModuleBase, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import ( + global_authors, + global_requirements, +) +from ansible_specdoc.objects import ( + FieldType, + SpecDocMeta, + SpecField, + SpecReturnValue, +) +from linode_api4 import ImageShareGroup, ImageShareGroupMemberToAdd + +SPEC = { + "label": SpecField( + type=FieldType.string, + description=["This Image Share Group Member's unique label."], + required=True, + ), + "token": SpecField( + type=FieldType.string, + description=[ + "A single-use Image Share Group Token provided by the Consumer. " + "This value is required when creating a member and is never returned." + ], + no_log=True, + ), + "sharegroup_id": SpecField( + type=FieldType.integer, + description=["The ID of the Image Share Group this member belongs to."], + required=True, + ), + "state": SpecField( + type=FieldType.string, + description=["The desired state of the target."], + choices=["present", "absent"], + required=True, + ), +} + +SPECDOC_META = SpecDocMeta( + description=["Manage an Image Share Group Member."], + requirements=global_requirements, + author=global_authors, + options=SPEC, + examples=docs.specdoc_examples, + return_values={ + "image_share_group_member": SpecReturnValue( + description="The Image Share Group Member in JSON serialized form.", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-member-token", + type=FieldType.dict, + sample=docs.result_image_share_group_member_samples, + ), + }, +) + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + + +class Module(LinodeModuleBase): + """Module for creating and destroying Image Share Group Members""" + + def __init__(self) -> None: + self.module_arg_spec = SPECDOC_META.ansible_spec + self.results = { + "changed": False, + "actions": [], + "image_share_group_member": None, + } + + super().__init__( + module_arg_spec=self.module_arg_spec, + required_if=[ + ("state", "present", ["token"]), + ], + ) + + def _get_image_share_group_member_by_label( + self, label: str + ) -> Optional[Dict[str, Any]]: + try: + sharegroup = self.client.load( + ImageShareGroup, self.module.params.get("sharegroup_id") + ) + return next( + ( + m.__dict__ + for m in sharegroup.get_members() + if m.label == label + ), + None, + ) + except Exception as exception: + return self.fail( + msg="failed to get image share group member for label {0}: {1}".format( + label, exception + ) + ) + + def _create(self) -> Optional[Dict[str, Any]]: + try: + sharegroup = self.client.load( + ImageShareGroup, + self.module.params.get("sharegroup_id"), + ) + + member = ImageShareGroupMemberToAdd( + token=self.module.params.get("token"), + label=self.module.params.get("label"), + ) + + member_obj = sharegroup.add_member(member) + return member_obj.__dict__ + + except Exception as exception: + return self.fail( + msg="failed to create Image Share Group Member: {0}".format( + exception + ) + ) + + def _handle_present(self) -> None: + label = self.module.params.get("label") + member = self._get_image_share_group_member_by_label(label) + + if not member: + member = self._create() + self.register_action( + "Created Image Share Group Member {0}".format(label) + ) + + self.results["image_share_group_member"] = member + + def _handle_absent(self) -> None: + label = self.module.params.get("label") + member = self._get_image_share_group_member_by_label(label) + + if member is None: + return + + sharegroup = self.client.load( + ImageShareGroup, + self.module.params.get("sharegroup_id"), + ) + + try: + token_uuid = member.get("token_uuid") + sharegroup.remove_member(token_uuid=token_uuid) + self.results["image_share_group_member"] = member + self.register_action(f"Deleted Image Share Group Member {label}") + except Exception as exception: + return self.fail( + msg="failed to delete Image Share Group Member {0}: {1}".format( + label, exception + ) + ) + + def exec_module(self, **kwargs: Any) -> Optional[dict]: + """Entrypoint for Image Share Group Member module""" + state = kwargs.get("state") + + if state == "absent": + self._handle_absent() + return self.results + + self._handle_present() + + return self.results + + +def main() -> None: + """Constructs and calls the Image Share Group Member module""" + Module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/image_share_group_token.py b/plugins/modules/image_share_group_token.py index 9fb4266a..ae3535d0 100644 --- a/plugins/modules/image_share_group_token.py +++ b/plugins/modules/image_share_group_token.py @@ -5,20 +5,21 @@ from __future__ import absolute_import, division, print_function -import ansible_collections.linode.cloud.plugins.module_utils.doc_fragments.image_share_group_token as docs -from typing import Optional, Any -from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( - filter_null_values, +from typing import Any, Optional + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_token as docs, ) from ansible_collections.linode.cloud.plugins.module_utils.linode_common import ( LinodeModuleBase, ) - from ansible_collections.linode.cloud.plugins.module_utils.linode_docs import ( global_authors, global_requirements, ) - +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + filter_null_values, +) from ansible_specdoc.objects import ( FieldType, SpecDocMeta, @@ -35,8 +36,9 @@ ), "valid_for_sharegroup_uuid": SpecField( type=FieldType.string, - description=["The UUID of the Image Share Group that this token is valid for."], - required=True, + description=[ + "The UUID of the Image Share Group that this token is valid for." + ], ), "state": SpecField( type=FieldType.string, @@ -61,10 +63,10 @@ ), "single_use_token": SpecReturnValue( description="The single use token string to provide to a Image Share Group Producer " - "to be added to the share group.", + "to be added to the share group.", type=FieldType.string, sample=docs.result_single_use_token_samples, - ) + ), }, ) @@ -88,33 +90,50 @@ def __init__(self) -> None: "single_use_token": None, } - super().__init__(module_arg_spec=self.module_arg_spec) + super().__init__( + module_arg_spec=self.module_arg_spec, + required_if=[ + ("state", "present", ["valid_for_sharegroup_uuid"]), + ], + ) - def _get_image_share_group_token_by_label(self, label: str) -> Optional[ImageShareGroupToken]: + def _get_image_share_group_token_by_label( + self, label: str + ) -> Optional[ImageShareGroupToken]: try: - return self.client.sharegroups.tokens(ImageShareGroupToken.label == label)[0] + return self.client.sharegroups.tokens( + ImageShareGroupToken.label == label + )[0] except IndexError: return None except Exception as exception: return self.fail( - msg="failed to get image share group token for label {0}: {1}".format(label, exception) + msg="failed to get image share group token for label {0}: {1}".format( + label, exception + ) ) def _create(self) -> Optional[ImageShareGroupToken]: params = filter_null_values( { "label": self.module.params.get("label"), - "valid_for_sharegroup_uuid": self.module.params.get("valid_for_sharegroup_uuid"), + "valid_for_sharegroup_uuid": self.module.params.get( + "valid_for_sharegroup_uuid" + ), } ) try: - token_obj, single_use_token = self.client.sharegroups.create_token(**params) + token_obj, single_use_token = self.client.sharegroups.create_token( + **params + ) self.results["single_use_token"] = single_use_token return token_obj except Exception as exception: return self.fail( - msg="failed to create Image Share Group Token: {0}".format(exception) + msg="failed to create Image Share Group Token: {0}".format( + exception + ) ) def _handle_present(self) -> None: @@ -123,7 +142,9 @@ def _handle_present(self) -> None: if not token: token = self._create() - self.register_action("Created Image Share Group Token {0}".format(label)) + self.register_action( + "Created Image Share Group Token {0}".format(label) + ) # Force lazy-loading token._api_get() @@ -137,7 +158,9 @@ def _handle_absent(self) -> None: if token is not None: self.results["image_share_group_token"] = token._raw_json token.delete() - self.register_action("Deleted image share group token {0}".format(label)) + self.register_action( + "Deleted image share group token {0}".format(label) + ) def exec_module(self, **kwargs: Any) -> Optional[dict]: """Entrypoint for Image Share Group Token module""" diff --git a/tests/integration/targets/image_share_group_member/tasks/main.yaml b/tests/integration/targets/image_share_group_member/tasks/main.yaml new file mode 100644 index 00000000..32850727 --- /dev/null +++ b/tests/integration/targets/image_share_group_member/tasks/main.yaml @@ -0,0 +1,102 @@ +- name: image_share_group_member + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert image share group is created + assert: + that: + - share_group_create.image_share_group.label == "ansible-test-" ~ r + - share_group_create.image_share_group.images_count == 0 + - share_group_create.image_share_group.members_count == 0 + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert image share group token is created + assert: + that: + - share_group_token_create.image_share_group_token.label == "ansible-test-" ~ r + - share_group_token_create.image_share_group_token.valid_for_sharegroup_uuid == share_group_create.image_share_group.uuid + - share_group_token_create.single_use_token is defined + - share_group_token_create.single_use_token | length > 0 + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + token: "{{ share_group_token_create.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert image share group member is added + assert: + that: + - share_group_member_add.image_share_group_member.label == "ansible-test-" ~ r + + always: + - ignore_errors: yes + block: + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file diff --git a/tests/integration/targets/image_share_group_token/tasks/main.yaml b/tests/integration/targets/image_share_group_token/tasks/main.yaml index 1f946c85..32ef1a9e 100644 --- a/tests/integration/targets/image_share_group_token/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_token/tasks/main.yaml @@ -50,7 +50,6 @@ - name: Delete image share group token linode.cloud.image_share_group_token: label: "{{ share_group_token_create.image_share_group_token.label }}" - valid_for_sharegroup_uuid: "{{ share_group_token_create.image_share_group_token.valid_for_sharegroup_uuid }}" state: absent when: - share_group_token_create is defined From 4a662d870428d27d62609a7e2aa875f1dd63c00f Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Tue, 30 Dec 2025 16:35:27 -0500 Subject: [PATCH 04/14] Added Image Share Group Info and List modules --- README.md | 2 + docs/modules/image_share_group_info.md | 52 +++++++++++++++ docs/modules/image_share_group_list.md | 62 ++++++++++++++++++ .../doc_fragments/image_share_group_info.py | 19 ++++++ .../doc_fragments/image_share_group_list.py | 20 ++++++ plugins/modules/image_share_group_info.py | 64 +++++++++++++++++++ plugins/modules/image_share_group_list.py | 34 ++++++++++ .../image_share_group_info/tasks/main.yaml | 35 ++++++++++ .../image_share_group_list/tasks/main.yaml | 62 ++++++++++++++++++ 9 files changed, 350 insertions(+) create mode 100644 docs/modules/image_share_group_info.md create mode 100644 docs/modules/image_share_group_list.md create mode 100644 plugins/module_utils/doc_fragments/image_share_group_info.py create mode 100644 plugins/module_utils/doc_fragments/image_share_group_list.py create mode 100644 plugins/modules/image_share_group_info.py create mode 100644 plugins/modules/image_share_group_list.py create mode 100644 tests/integration/targets/image_share_group_info/tasks/main.yaml create mode 100644 tests/integration/targets/image_share_group_list/tasks/main.yaml diff --git a/README.md b/README.md index 0e8b9506..04da3458 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Name | Description | [linode.cloud.firewall_settings_info](./docs/modules/firewall_settings_info.md)|Get info about a Linode Firewall Settings.| [linode.cloud.firewall_template_info](./docs/modules/firewall_template_info.md)|Get info about a Linode Firewall Template.| [linode.cloud.image_info](./docs/modules/image_info.md)|Get info about a Linode Image.| +[linode.cloud.image_share_group_info](./docs/modules/image_share_group_info.md)|Get info about a Linode Image Share Group.| [linode.cloud.instance_info](./docs/modules/instance_info.md)|Get info about a Linode Instance.| [linode.cloud.instance_interface_settings_info](./docs/modules/instance_interface_settings_info.md)|Get the interface settings for a Linode instance.| [linode.cloud.ip_info](./docs/modules/ip_info.md)|Get info about a Linode IP.| @@ -115,6 +116,7 @@ Name | Description | [linode.cloud.firewall_list](./docs/modules/firewall_list.md)|List and filter on Firewalls.| [linode.cloud.firewall_template_list](./docs/modules/firewall_template_list.md)|List and filter on Firewall Templates.| [linode.cloud.image_list](./docs/modules/image_list.md)|List and filter on Images.| +[linode.cloud.image_share_group_list](./docs/modules/image_share_group_list.md)|List and filter on Image Share Groups.| [linode.cloud.instance_interface_firewall_list](./docs/modules/instance_interface_firewall_list.md)|List and filter on Linode Interface Firewalls.| [linode.cloud.instance_list](./docs/modules/instance_list.md)|List and filter on Instances.| [linode.cloud.instance_type_list](./docs/modules/instance_type_list.md)|**NOTE: This module has been deprecated in favor of `type_list`.**| diff --git a/docs/modules/image_share_group_info.md b/docs/modules/image_share_group_info.md new file mode 100644 index 00000000..7ee3b691 --- /dev/null +++ b/docs/modules/image_share_group_info.md @@ -0,0 +1,52 @@ +# image_share_group_info + +Get info about a Linode Image Share Group. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Get info about an Image Share Group by label + linode.cloud.image_share_group_info: + label: example-image-share-group +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `id` |
`int`
|
Optional
| The ID of the Image Share Group to resolve. **(Conflicts With: `label`)** | +| `label` |
`str`
|
Optional
| The label of the Image Share Group to resolve. **(Conflicts With: `id`)** | + +## Return Values + +- `image_share_group` - The returned Image Share Group. + + - Sample Response: + ```json + { + "created": "2025-04-14T22:44:02", + "description": "Example.", + "expiry": "2025-04-14T22:44:02", + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "example-image-share-group", + "members_count": 0, + "updated": "2025-04-14T22:44:02", + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup) for a list of returned fields + + diff --git a/docs/modules/image_share_group_list.md b/docs/modules/image_share_group_list.md new file mode 100644 index 00000000..683ba06b --- /dev/null +++ b/docs/modules/image_share_group_list.md @@ -0,0 +1,62 @@ +# image_share_group_list + +List and filter on Image Share Groups. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all of the Image Share Groups for the current Linode Account + linode.cloud.image_share_group_list: {} +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `order` |
`str`
|
Optional
| The order to list Image Share Groups in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order Image Share Groups by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Groups. | +| `count` |
`int`
|
Optional
| The number of Image Share Groups to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-sharegroups). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `image_share_groups` - The returned Image Share Groups. + + - Sample Response: + ```json + [ + { + "created": "2025-04-14T22:44:02", + "description": "Group of base operating system images and engineers used for CI/CD pipelines and infrastructure automation", + "expiry": null, + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "DevOps Base Images", + "members_count": 0, + "updated": null, + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroups) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/image_share_group_info.py b/plugins/module_utils/doc_fragments/image_share_group_info.py new file mode 100644 index 00000000..baa6e402 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_info.py @@ -0,0 +1,19 @@ +"""Documentation fragments for the image_share_group_info module""" + +specdoc_examples = [''' +- name: Get info about an Image Share Group by label + linode.cloud.image_share_group_info: + label: example-image-share-group'''] + +result_image_share_group_samples = ['''{ + "created": "2025-04-14T22:44:02", + "description": "Example.", + "expiry": "2025-04-14T22:44:02", + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "example-image-share-group", + "members_count": 0, + "updated": "2025-04-14T22:44:02", + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" +}'''] diff --git a/plugins/module_utils/doc_fragments/image_share_group_list.py b/plugins/module_utils/doc_fragments/image_share_group_list.py new file mode 100644 index 00000000..40ae8818 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_list.py @@ -0,0 +1,20 @@ +"""Documentation fragments for the image_share_group_list module""" + +specdoc_examples = [''' +- name: List all of the Image Share Groups for the current Linode Account + linode.cloud.image_share_group_list: {}'''] + +result_image_share_groups_samples = ['''[ + { + "created": "2025-04-14T22:44:02", + "description": "Group of base operating system images and engineers used for CI/CD pipelines and infrastructure automation", + "expiry": null, + "id": 1, + "images_count": 0, + "is_suspended": false, + "label": "DevOps Base Images", + "members_count": 0, + "updated": null, + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" + } +]'''] diff --git a/plugins/modules/image_share_group_info.py b/plugins/modules/image_share_group_info.py new file mode 100644 index 00000000..ff707f97 --- /dev/null +++ b/plugins/modules/image_share_group_info.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the functionality for the Image Share Group info module.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_info as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( + InfoModule, + InfoModuleAttr, + InfoModuleResult, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + safe_find, +) +from ansible_specdoc.objects import FieldType +from linode_api4 import ImageShareGroup + +module = InfoModule( + primary_result=InfoModuleResult( + field_name="image_share_group", + field_type=FieldType.dict, + display_name="Image Share Group", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup", + samples=docs.result_image_share_group_samples, + ), + attributes=[ + InfoModuleAttr( + display_name="ID", + name="id", + type=FieldType.integer, + get=lambda client, params: client.load( + ImageShareGroup, + params.get("id"), + )._raw_json, + ), + InfoModuleAttr( + display_name="label", + name="label", + type=FieldType.string, + get=lambda client, params: safe_find( + client.sharegroups, + ImageShareGroup.label == params.get("label"), + raise_not_found=True, + )._raw_json, + ), + ], + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/plugins/modules/image_share_group_list.py b/plugins/modules/image_share_group_list.py new file mode 100644 index 00000000..c10db6f3 --- /dev/null +++ b/plugins/modules/image_share_group_list.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module allows users to list Image Share Groups.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_list as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, +) + +module = ListModule( + result_display_name="Image Share Groups", + result_field_name="image_share_groups", + endpoint_template="/images/sharegroups", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroups", + result_samples=docs.result_image_share_groups_samples, + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/image_share_group_info/tasks/main.yaml b/tests/integration/targets/image_share_group_info/tasks/main.yaml new file mode 100644 index 00000000..4d9192d3 --- /dev/null +++ b/tests/integration/targets/image_share_group_info/tasks/main.yaml @@ -0,0 +1,35 @@ +- name: image_share_group_info + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create an image share group + linode.cloud.image_share_group: + label: 'ansible-test-{{ r }}' + state: present + register: share_group_create + + - name: Get information about the Image Share Group + linode.cloud.image_share_group_info: + label: "{{ share_group_create.image_share_group.label }}" + register: share_group_info + + - name: Assert Image Share Group information + assert: + that: + - share_group_info.image_share_group.label == "ansible-test-" ~ r + + always: + - ignore_errors: yes + block: + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_create.image_share_group.label }}' + state: absent + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' \ No newline at end of file diff --git a/tests/integration/targets/image_share_group_list/tasks/main.yaml b/tests/integration/targets/image_share_group_list/tasks/main.yaml new file mode 100644 index 00000000..62c9105a --- /dev/null +++ b/tests/integration/targets/image_share_group_list/tasks/main.yaml @@ -0,0 +1,62 @@ +- name: image_share_group_list + block: + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create an image share group + linode.cloud.image_share_group: + label: 'ansible-test-{{ r }}-one' + state: present + register: share_group_one_create + + - name: Create another image share group + linode.cloud.image_share_group: + label: 'ansible-test-{{ r }}-two' + state: present + register: share_group_two_create + + - name: List Image Share Groups with no filter + linode.cloud.image_share_group_list: + count: 2 + register: no_filter + + - name: Assert image_share_group_list with no filter + assert: + that: + - no_filter.image_share_groups | length == 2 + + - name: List Image Share Groups with filter on label + linode.cloud.image_share_group_list: + count: 1 + order_by: created + order: desc + filters: + - name: label + values: 'ansible-test-{{ r }}-one' + register: filter + + - name: Assert image_share_group_list with filter on label + assert: + that: + - filter.image_share_groups | length == 1 + - filter.image_share_groups[0].label == "ansible-test-" ~ r ~ "-one" + + always: + - ignore_errors: yes + block: + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_one_create.image_share_group.label }}' + state: absent + + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_two_create.image_share_group.label }}' + state: absent + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' From 322223b26148d0ce1ac06e04fa53a00883a173c2 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 10:14:15 -0500 Subject: [PATCH 05/14] Added Image Share Group Token Info and List modules --- README.md | 2 + docs/modules/image_share_group_token_info.md | 51 ++++++++ docs/modules/image_share_group_token_list.md | 61 ++++++++++ .../image_share_group_token_info.py | 18 +++ .../image_share_group_token_list.py | 19 +++ .../modules/image_share_group_token_info.py | 64 ++++++++++ .../modules/image_share_group_token_list.py | 34 ++++++ .../tasks/main.yaml | 85 ++++++++++++++ .../tasks/main.yaml | 110 ++++++++++++++++++ 9 files changed, 444 insertions(+) create mode 100644 docs/modules/image_share_group_token_info.md create mode 100644 docs/modules/image_share_group_token_list.md create mode 100644 plugins/module_utils/doc_fragments/image_share_group_token_info.py create mode 100644 plugins/module_utils/doc_fragments/image_share_group_token_list.py create mode 100644 plugins/modules/image_share_group_token_info.py create mode 100644 plugins/modules/image_share_group_token_list.py create mode 100644 tests/integration/targets/image_share_group_token_info/tasks/main.yaml create mode 100644 tests/integration/targets/image_share_group_token_list/tasks/main.yaml diff --git a/README.md b/README.md index 04da3458..bfdebf27 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Name | Description | [linode.cloud.firewall_template_info](./docs/modules/firewall_template_info.md)|Get info about a Linode Firewall Template.| [linode.cloud.image_info](./docs/modules/image_info.md)|Get info about a Linode Image.| [linode.cloud.image_share_group_info](./docs/modules/image_share_group_info.md)|Get info about a Linode Image Share Group.| +[linode.cloud.image_share_group_token_info](./docs/modules/image_share_group_token_info.md)|Get info about a Linode Image Share Group Token.| [linode.cloud.instance_info](./docs/modules/instance_info.md)|Get info about a Linode Instance.| [linode.cloud.instance_interface_settings_info](./docs/modules/instance_interface_settings_info.md)|Get the interface settings for a Linode instance.| [linode.cloud.ip_info](./docs/modules/ip_info.md)|Get info about a Linode IP.| @@ -117,6 +118,7 @@ Name | Description | [linode.cloud.firewall_template_list](./docs/modules/firewall_template_list.md)|List and filter on Firewall Templates.| [linode.cloud.image_list](./docs/modules/image_list.md)|List and filter on Images.| [linode.cloud.image_share_group_list](./docs/modules/image_share_group_list.md)|List and filter on Image Share Groups.| +[linode.cloud.image_share_group_token_list](./docs/modules/image_share_group_token_list.md)|List and filter on Image Share Group Tokens.| [linode.cloud.instance_interface_firewall_list](./docs/modules/instance_interface_firewall_list.md)|List and filter on Linode Interface Firewalls.| [linode.cloud.instance_list](./docs/modules/instance_list.md)|List and filter on Instances.| [linode.cloud.instance_type_list](./docs/modules/instance_type_list.md)|**NOTE: This module has been deprecated in favor of `type_list`.**| diff --git a/docs/modules/image_share_group_token_info.md b/docs/modules/image_share_group_token_info.md new file mode 100644 index 00000000..482d2a32 --- /dev/null +++ b/docs/modules/image_share_group_token_info.md @@ -0,0 +1,51 @@ +# image_share_group_token_info + +Get info about a Linode Image Share Group Token. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Get info about an Image Share Group Token by label + linode.cloud.image_share_group_token_info: + label: example-image-share-group-token +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `token_uuid` |
`str`
|
Optional
| The Token UUID of the Image Share Group Token to resolve. **(Conflicts With: `label`)** | +| `label` |
`str`
|
Optional
| The label of the Image Share Group Token to resolve. **(Conflicts With: `token_uuid`)** | + +## Return Values + +- `image_share_group_token` - The returned Image Share Group Token. + + - Sample Response: + ```json + { + "created": "2025-08-04T10:09:09", + "expiry": "2025-08-04T10:09:11", + "label": "example-token", + "sharegroup_label": "example-sharegroup", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": "2025-08-04T10:09:10", + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-token) for a list of returned fields + + diff --git a/docs/modules/image_share_group_token_list.md b/docs/modules/image_share_group_token_list.md new file mode 100644 index 00000000..b5f1120a --- /dev/null +++ b/docs/modules/image_share_group_token_list.md @@ -0,0 +1,61 @@ +# image_share_group_token_list + +List and filter on Image Share Group Tokens. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all of the Image Share Group Tokens for the current Linode Account + linode.cloud.image_share_group_token_list: {} +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `order` |
`str`
|
Optional
| The order to list Image Share Group Tokens in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order Image Share Group Tokens by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Group Tokens. | +| `count` |
`int`
|
Optional
| The number of Image Share Group Tokens to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-user-tokens). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `image_share_group_tokens` - The returned Image Share Group Tokens. + + - Sample Response: + ```json + [ + { + "created": "2025-08-04T10:09:09", + "expiry": "2025-08-04T10:09:11", + "label": "example-token", + "sharegroup_label": "example-sharegroup", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": "2025-08-04T10:09:10", + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-user-tokens) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/image_share_group_token_info.py b/plugins/module_utils/doc_fragments/image_share_group_token_info.py new file mode 100644 index 00000000..c9a2d772 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_token_info.py @@ -0,0 +1,18 @@ +"""Documentation fragments for the image_share_group_token_info module""" + +specdoc_examples = [''' +- name: Get info about an Image Share Group Token by label + linode.cloud.image_share_group_token_info: + label: example-image-share-group-token'''] + +result_image_share_group_token_samples = ['''{ + "created": "2025-08-04T10:09:09", + "expiry": "2025-08-04T10:09:11", + "label": "example-token", + "sharegroup_label": "example-sharegroup", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": "2025-08-04T10:09:10", + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" +}'''] diff --git a/plugins/module_utils/doc_fragments/image_share_group_token_list.py b/plugins/module_utils/doc_fragments/image_share_group_token_list.py new file mode 100644 index 00000000..495b44d1 --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_token_list.py @@ -0,0 +1,19 @@ +"""Documentation fragments for the image_share_group_token_list module""" + +specdoc_examples = [''' +- name: List all of the Image Share Group Tokens for the current Linode Account + linode.cloud.image_share_group_token_list: {}'''] + +result_image_share_group_tokens_samples = ['''[ + { + "created": "2025-08-04T10:09:09", + "expiry": "2025-08-04T10:09:11", + "label": "example-token", + "sharegroup_label": "example-sharegroup", + "sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359", + "status": "active", + "token_uuid": "13428362-5458-4dad-b14b-8d0d4d648f8c", + "updated": "2025-08-04T10:09:10", + "valid_for_sharegroup_uuid": "e1d0e58b-f89f-4237-84ab-b82077342359" + } +]'''] diff --git a/plugins/modules/image_share_group_token_info.py b/plugins/modules/image_share_group_token_info.py new file mode 100644 index 00000000..7d8ad58e --- /dev/null +++ b/plugins/modules/image_share_group_token_info.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the functionality for the Image Share Group Token info module.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_token_info as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( + InfoModule, + InfoModuleAttr, + InfoModuleResult, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_helper import ( + safe_find, +) +from ansible_specdoc.objects import FieldType +from linode_api4 import ImageShareGroupToken + +module = InfoModule( + primary_result=InfoModuleResult( + field_name="image_share_group_token", + field_type=FieldType.dict, + display_name="Image Share Group Token", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-token", + samples=docs.result_image_share_group_token_samples, + ), + attributes=[ + InfoModuleAttr( + display_name="Token UUID", + name="token_uuid", + type=FieldType.string, + get=lambda client, params: client.load( + ImageShareGroupToken, + params.get("token_uuid"), + )._raw_json, + ), + InfoModuleAttr( + display_name="label", + name="label", + type=FieldType.string, + get=lambda client, params: safe_find( + client.sharegroups.tokens, + ImageShareGroupToken.label == params.get("label"), + raise_not_found=True, + )._raw_json, + ), + ], + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/plugins/modules/image_share_group_token_list.py b/plugins/modules/image_share_group_token_list.py new file mode 100644 index 00000000..7aa0fdbe --- /dev/null +++ b/plugins/modules/image_share_group_token_list.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module allows users to list Image Share Group Tokens.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_token_list as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, +) + +module = ListModule( + result_display_name="Image Share Group Tokens", + result_field_name="image_share_group_tokens", + endpoint_template="/images/sharegroups/tokens", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-user-tokens", + result_samples=docs.result_image_share_group_tokens_samples, + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/image_share_group_token_info/tasks/main.yaml b/tests/integration/targets/image_share_group_token_info/tasks/main.yaml new file mode 100644 index 00000000..940a6c91 --- /dev/null +++ b/tests/integration/targets/image_share_group_token_info/tasks/main.yaml @@ -0,0 +1,85 @@ +- name: image_share_group_token_info + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Get information about the Image Share Group Token by label + linode.cloud.image_share_group_token_info: + label: "{{ share_group_token_create.image_share_group_token.label }}" + register: share_group_token_info_label + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert Image Share Group Token information + assert: + that: + - share_group_token_info_label.image_share_group_token.label == "ansible-test-" ~ r + + - name: Get information about the Image Share Group Token by Token UUID + linode.cloud.image_share_group_token_info: + token_uuid: "{{ share_group_token_create.image_share_group_token.token_uuid }}" + register: share_group_token_info_token_uuid + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert Image Share Group Token information + assert: + that: + - share_group_token_info_token_uuid.image_share_group_token.label == "ansible-test-" ~ r + + always: + - ignore_errors: yes + block: + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file diff --git a/tests/integration/targets/image_share_group_token_list/tasks/main.yaml b/tests/integration/targets/image_share_group_token_list/tasks/main.yaml new file mode 100644 index 00000000..900b37b8 --- /dev/null +++ b/tests/integration/targets/image_share_group_token_list/tasks/main.yaml @@ -0,0 +1,110 @@ +- name: image_share_group_token_info + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}-one" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create_one + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Create another image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}-two" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create_two + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: List Image Share Group Tokens with no filter + linode.cloud.image_share_group_token_list: + count: 2 + register: no_filter + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert image_share_group_token_list with no filter + assert: + that: + - no_filter.image_share_group_tokens | length == 2 + + - name: List Image Share Group Tokens with filter on label + linode.cloud.image_share_group_token_list: + count: 1 + order_by: created + order: desc + filters: + - name: label + values: 'ansible-test-{{ r }}-one' + register: filter + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert image_share_group_token_list with filter on label + assert: + that: + - filter.image_share_group_tokens | length == 1 + - filter.image_share_group_tokens[0].label == "ansible-test-" ~ r ~ "-one" + + always: + - ignore_errors: yes + block: + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create_one.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create_one is defined + - share_group_token_create_one.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create_two.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create_two is defined + - share_group_token_create_two.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file From 6441486ca3e8a3f16f6fd7a697386fe0fb637f5c Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 11:48:02 -0500 Subject: [PATCH 06/14] Added Image Share Group Member Info and List modules --- README.md | 2 + docs/modules/image_share_group_member_info.md | 50 ++++++ docs/modules/image_share_group_member_list.md | 60 +++++++ .../image_share_group_member_info.py | 16 ++ .../image_share_group_member_list.py | 17 ++ .../modules/image_share_group_member_info.py | 82 ++++++++++ .../modules/image_share_group_member_list.py | 43 +++++ .../tasks/main.yaml | 108 ++++++++++++ .../tasks/main.yaml | 154 ++++++++++++++++++ 9 files changed, 532 insertions(+) create mode 100644 docs/modules/image_share_group_member_info.md create mode 100644 docs/modules/image_share_group_member_list.md create mode 100644 plugins/module_utils/doc_fragments/image_share_group_member_info.py create mode 100644 plugins/module_utils/doc_fragments/image_share_group_member_list.py create mode 100644 plugins/modules/image_share_group_member_info.py create mode 100644 plugins/modules/image_share_group_member_list.py create mode 100644 tests/integration/targets/image_share_group_member_info/tasks/main.yaml create mode 100644 tests/integration/targets/image_share_group_member_list/tasks/main.yaml diff --git a/README.md b/README.md index bfdebf27..3194c45b 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Name | Description | [linode.cloud.firewall_template_info](./docs/modules/firewall_template_info.md)|Get info about a Linode Firewall Template.| [linode.cloud.image_info](./docs/modules/image_info.md)|Get info about a Linode Image.| [linode.cloud.image_share_group_info](./docs/modules/image_share_group_info.md)|Get info about a Linode Image Share Group.| +[linode.cloud.image_share_group_member_info](./docs/modules/image_share_group_member_info.md)|Get info about a Linode Image Share Group Member.| [linode.cloud.image_share_group_token_info](./docs/modules/image_share_group_token_info.md)|Get info about a Linode Image Share Group Token.| [linode.cloud.instance_info](./docs/modules/instance_info.md)|Get info about a Linode Instance.| [linode.cloud.instance_interface_settings_info](./docs/modules/instance_interface_settings_info.md)|Get the interface settings for a Linode instance.| @@ -118,6 +119,7 @@ Name | Description | [linode.cloud.firewall_template_list](./docs/modules/firewall_template_list.md)|List and filter on Firewall Templates.| [linode.cloud.image_list](./docs/modules/image_list.md)|List and filter on Images.| [linode.cloud.image_share_group_list](./docs/modules/image_share_group_list.md)|List and filter on Image Share Groups.| +[linode.cloud.image_share_group_member_list](./docs/modules/image_share_group_member_list.md)|List and filter on Image Share Group Members.| [linode.cloud.image_share_group_token_list](./docs/modules/image_share_group_token_list.md)|List and filter on Image Share Group Tokens.| [linode.cloud.instance_interface_firewall_list](./docs/modules/instance_interface_firewall_list.md)|List and filter on Linode Interface Firewalls.| [linode.cloud.instance_list](./docs/modules/instance_list.md)|List and filter on Instances.| diff --git a/docs/modules/image_share_group_member_info.md b/docs/modules/image_share_group_member_info.md new file mode 100644 index 00000000..832a14b4 --- /dev/null +++ b/docs/modules/image_share_group_member_info.md @@ -0,0 +1,50 @@ +# image_share_group_member_info + +Get info about a Linode Image Share Group Member. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Get info about an Image Share Group Member by label + linode.cloud.image_share_group_member_info: + sharegroup_id: 123456 + label: example-image-share-group-member +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `sharegroup_id` |
`int`
|
**Required**
| The ID of the Image Share Group for this resource. | +| `token_uuid` |
`str`
|
Optional
| The Token UUID of the Image Share Group Member to resolve. **(Conflicts With: `label`)** | +| `label` |
`str`
|
Optional
| The label of the Image Share Group Member to resolve. **(Conflicts With: `token_uuid`)** | + +## Return Values + +- `image_share_group_member` - The returned Image Share Group Member. + + - Sample Response: + ```json + { + "created": "2025-08-04T10:07:59", + "expiry": "2025-08-04T10:08:01", + "label": "Engineering - Backend", + "status": "active", + "token_uuid": "4591075e-4ba8-43c9-a521-928c3d4a135d", + "updated": "2025-08-04T10:08:00" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-member-token) for a list of returned fields + + diff --git a/docs/modules/image_share_group_member_list.md b/docs/modules/image_share_group_member_list.md new file mode 100644 index 00000000..b8c9b43a --- /dev/null +++ b/docs/modules/image_share_group_member_list.md @@ -0,0 +1,60 @@ +# image_share_group_member_list + +List and filter on Image Share Group Members. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all of the Image Share Group Members for the specified Share Group + linode.cloud.image_share_group_member_list: + sharegroup_id: 123 +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `sharegroup_id` |
`int`
|
**Required**
| The parent Image Share Group for the Image Share Group Members. | +| `order` |
`str`
|
Optional
| The order to list Image Share Group Members in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order Image Share Group Members by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Group Members. | +| `count` |
`int`
|
Optional
| The number of Image Share Group Members to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-members). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `image_share_group_members` - The returned Image Share Group Members. + + - Sample Response: + ```json + [ + { + "created": "2025-08-04T10:07:59", + "expiry": "2025-08-04T10:08:01", + "label": "member-label", + "status": "active", + "token_uuid": "4591075e-4ba8-43c9-a521-928c3d4a135d", + "updated": "2025-08-04T10:08:00" + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-members) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/image_share_group_member_info.py b/plugins/module_utils/doc_fragments/image_share_group_member_info.py new file mode 100644 index 00000000..1e5670ec --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_member_info.py @@ -0,0 +1,16 @@ +"""Documentation fragments for the image_share_group_member_info module""" + +specdoc_examples = [''' +- name: Get info about an Image Share Group Member by label + linode.cloud.image_share_group_member_info: + sharegroup_id: 123456 + label: example-image-share-group-member'''] + +result_image_share_group_member_samples = ['''{ + "created": "2025-08-04T10:07:59", + "expiry": "2025-08-04T10:08:01", + "label": "Engineering - Backend", + "status": "active", + "token_uuid": "4591075e-4ba8-43c9-a521-928c3d4a135d", + "updated": "2025-08-04T10:08:00" +}'''] diff --git a/plugins/module_utils/doc_fragments/image_share_group_member_list.py b/plugins/module_utils/doc_fragments/image_share_group_member_list.py new file mode 100644 index 00000000..594f49bb --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_member_list.py @@ -0,0 +1,17 @@ +"""Documentation fragments for the image_share_group_member_list module""" + +specdoc_examples = [''' +- name: List all of the Image Share Group Members for the specified Share Group + linode.cloud.image_share_group_member_list: + sharegroup_id: 123'''] + +result_image_share_group_members_samples = ['''[ + { + "created": "2025-08-04T10:07:59", + "expiry": "2025-08-04T10:08:01", + "label": "member-label", + "status": "active", + "token_uuid": "4591075e-4ba8-43c9-a521-928c3d4a135d", + "updated": "2025-08-04T10:08:00" + } +]'''] diff --git a/plugins/modules/image_share_group_member_info.py b/plugins/modules/image_share_group_member_info.py new file mode 100644 index 00000000..44a7c9f0 --- /dev/null +++ b/plugins/modules/image_share_group_member_info.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the functionality for the Image Share Group Member info module.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_member_info as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( + InfoModule, + InfoModuleAttr, + InfoModuleParam, + InfoModuleResult, +) +from ansible_specdoc.objects import FieldType +from linode_api4 import ImageShareGroup + +module = InfoModule( + primary_result=InfoModuleResult( + field_name="image_share_group_member", + field_type=FieldType.dict, + display_name="Image Share Group Member", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-member-token", + samples=docs.result_image_share_group_member_samples, + ), + params=[ + InfoModuleParam( + display_name="Image Share Group", + name="sharegroup_id", + type=FieldType.integer, + ) + ], + attributes=[ + InfoModuleAttr( + display_name="Token UUID", + name="token_uuid", + type=FieldType.string, + get=lambda client, params: next( + ( + m.__dict__ + for m in client.load( + ImageShareGroup, + params.get("sharegroup_id"), + ).get_members() + if m.token_uuid == params.get("token_uuid") + ), + None, + ), + ), + InfoModuleAttr( + display_name="label", + name="label", + type=FieldType.string, + get=lambda client, params: next( + ( + m.__dict__ + for m in client.load( + ImageShareGroup, + params.get("sharegroup_id"), + ).get_members() + if m.label == params.get("label") + ), + None, + ), + ), + ], + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/plugins/modules/image_share_group_member_list.py b/plugins/modules/image_share_group_member_list.py new file mode 100644 index 00000000..9a617296 --- /dev/null +++ b/plugins/modules/image_share_group_member_list.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module allows users to list Image Share Group Members.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_member_list as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, + ListModuleParam, +) +from ansible_specdoc.objects import FieldType + +module = ListModule( + result_display_name="Image Share Group Members", + result_field_name="image_share_group_members", + endpoint_template="/images/sharegroups/{sharegroup_id}/members", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-members", + result_samples=docs.result_image_share_group_members_samples, + examples=docs.specdoc_examples, + params=[ + ListModuleParam( + display_name="Image Share Group", + name="sharegroup_id", + type=FieldType.integer, + ), + ], +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/image_share_group_member_info/tasks/main.yaml b/tests/integration/targets/image_share_group_member_info/tasks/main.yaml new file mode 100644 index 00000000..c7a96d1e --- /dev/null +++ b/tests/integration/targets/image_share_group_member_info/tasks/main.yaml @@ -0,0 +1,108 @@ +- name: image_share_group_member_list + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + token: "{{ share_group_token_create.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Get information about the Image Share Group Member by label + linode.cloud.image_share_group_member_info: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + label: "{{ share_group_member_add.image_share_group_member.label }}" + register: share_group_member_info_label + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert Image Share Group Member information + assert: + that: + - share_group_member_info_label.image_share_group_member.label == "ansible-test-" ~ r + + - name: Get information about the Image Share Group Member by token_uuid + linode.cloud.image_share_group_member_info: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + token_uuid: "{{ share_group_member_add.image_share_group_member.token_uuid }}" + register: share_group_member_info_token_uuid + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert Image Share Group Member information + assert: + that: + - share_group_member_info_token_uuid.image_share_group_member.label == "ansible-test-" ~ r + + always: + - ignore_errors: yes + block: + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file diff --git a/tests/integration/targets/image_share_group_member_list/tasks/main.yaml b/tests/integration/targets/image_share_group_member_list/tasks/main.yaml new file mode 100644 index 00000000..8ddbb859 --- /dev/null +++ b/tests/integration/targets/image_share_group_member_list/tasks/main.yaml @@ -0,0 +1,154 @@ +- name: image_share_group_member_list + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}-one" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create_one + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Create another image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}-two" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create_two + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}-one" + token: "{{ share_group_token_create_one.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add_one + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}-two" + token: "{{ share_group_token_create_two.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add_two + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: List image share group members with no filter + linode.cloud.image_share_group_member_list: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + count: 2 + register: no_filter + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert image_share_group_member_list with no filter + assert: + that: + - no_filter.image_share_group_members | length == 2 + + - name: List image share group members with filter on label + linode.cloud.image_share_group_member_list: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + count: 1 + order_by: created + order: desc + filters: + - name: label + values: 'ansible-test-{{ r }}-two' + register: filter + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert image_share_group_member_list with filter on label + assert: + that: + - filter.image_share_group_members | length == 1 + - filter.image_share_group_members[0].label == "ansible-test-" ~ r ~ "-two" + + always: + - ignore_errors: yes + block: + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}-one" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}-two" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create_one.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create_two.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file From 2da19813a806f1ec92fb0b7dddde8ee26dc5fe02 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 13:44:41 -0500 Subject: [PATCH 07/14] Added Image Share Group Images List module --- README.md | 1 + docs/modules/image.md | 50 +++--- docs/modules/image_info.md | 50 +++--- docs/modules/image_list.md | 62 +++++--- docs/modules/image_share_group_image_list.md | 93 +++++++++++ plugins/module_utils/doc_fragments/image.py | 50 +++--- .../module_utils/doc_fragments/image_list.py | 62 +++++--- .../image_share_group_image_list.py | 50 ++++++ .../modules/image_share_group_image_list.py | 43 ++++++ .../image_share_group_basic/tasks/main.yaml | 5 + .../tasks/main.yaml | 145 ++++++++++++++++++ 11 files changed, 501 insertions(+), 110 deletions(-) create mode 100644 docs/modules/image_share_group_image_list.md create mode 100644 plugins/module_utils/doc_fragments/image_share_group_image_list.py create mode 100644 plugins/modules/image_share_group_image_list.py create mode 100644 tests/integration/targets/image_share_group_image_list/tasks/main.yaml diff --git a/README.md b/README.md index 3194c45b..6326ed21 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Name | Description | [linode.cloud.firewall_list](./docs/modules/firewall_list.md)|List and filter on Firewalls.| [linode.cloud.firewall_template_list](./docs/modules/firewall_template_list.md)|List and filter on Firewall Templates.| [linode.cloud.image_list](./docs/modules/image_list.md)|List and filter on Images.| +[linode.cloud.image_share_group_image_list](./docs/modules/image_share_group_image_list.md)|List and filter on Image Share Group Images.| [linode.cloud.image_share_group_list](./docs/modules/image_share_group_list.md)|List and filter on Image Share Groups.| [linode.cloud.image_share_group_member_list](./docs/modules/image_share_group_member_list.md)|List and filter on Image Share Group Members.| [linode.cloud.image_share_group_token_list](./docs/modules/image_share_group_token_list.md)|List and filter on Image Share Group Tokens.| diff --git a/docs/modules/image.md b/docs/modules/image.md index d16b3835..ea9aa386 100644 --- a/docs/modules/image.md +++ b/docs/modules/image.md @@ -83,33 +83,43 @@ Manage a Linode Image. - Sample Response: ```json { - "capabilities": [], + "capabilities": [ + "cloud-init", + "distributed-sites" + ], "created": "2021-08-14T22:44:02", - "created_by": "my-account", + "created_by": "linode", "deprecated": false, - "description": "Example Image description.", + "description": "Example image description.", "eol": "2026-07-01T04:00:00", "expiry": null, - "id": "private/123", - "is_public": true, - "label": "my-image", - "size": 2500, - "status": null, - "type": "manual", - "updated": "2021-08-14T22:44:02", - "vendor": "Debian", - "tags": ["test"], - "total_size": 5000, + "id": "private/15", + "image_sharing": { + "shared_by": null, + "shared_with": { + "sharegroup_count": 0, + "sharegroup_list_url": "/images/private/15/sharegroups" + } + }, + "is_public": false, + "is_shared": false, + "label": "Debian 11", "regions": [ { - "region": "us-east", - "status": "available" - }, - { - "region": "us-central", - "status": "pending" + "region": "us-iad", + "status": "available" } - ] + ], + "size": 2500, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 1234567, + "type": "manual", + "updated": "2021-08-14T22:44:02", + "vendor": "Debian" } ``` - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-image) for a list of returned fields diff --git a/docs/modules/image_info.md b/docs/modules/image_info.md index abf339b4..0fdf84d9 100644 --- a/docs/modules/image_info.md +++ b/docs/modules/image_info.md @@ -41,33 +41,43 @@ Get info about a Linode Image. - Sample Response: ```json { - "capabilities": [], + "capabilities": [ + "cloud-init", + "distributed-sites" + ], "created": "2021-08-14T22:44:02", - "created_by": "my-account", + "created_by": "linode", "deprecated": false, - "description": "Example Image description.", + "description": "Example image description.", "eol": "2026-07-01T04:00:00", "expiry": null, - "id": "private/123", - "is_public": true, - "label": "my-image", - "size": 2500, - "status": null, - "type": "manual", - "updated": "2021-08-14T22:44:02", - "vendor": "Debian", - "tags": ["test"], - "total_size": 5000, + "id": "private/15", + "image_sharing": { + "shared_by": null, + "shared_with": { + "sharegroup_count": 0, + "sharegroup_list_url": "/images/private/15/sharegroups" + } + }, + "is_public": false, + "is_shared": false, + "label": "Debian 11", "regions": [ { - "region": "us-east", - "status": "available" - }, - { - "region": "us-central", - "status": "pending" + "region": "us-iad", + "status": "available" } - ] + ], + "size": 2500, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 1234567, + "type": "manual", + "updated": "2021-08-14T22:44:02", + "vendor": "Debian" } ``` - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-image) for a list of returned fields diff --git a/docs/modules/image_list.md b/docs/modules/image_list.md index 03345cf8..9f67eac6 100644 --- a/docs/modules/image_list.md +++ b/docs/modules/image_list.md @@ -59,33 +59,45 @@ List and filter on Images. - Sample Response: ```json [ - { - "created":"2021-08-14T22:44:02", - "created_by":"my-account", - "deprecated":false, - "description":"Example Image description.", - "eol":"2026-07-01T04:00:00", - "expiry":null, - "id":"private/123", - "is_public":false, - "label":"test", - "size":2500, - "status":null, - "type":"manual", - "updated":"2021-08-14T22:44:02", - "vendor":"Debian", - "tags": ["test"], - "total_size": 5000, + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2021-08-14T22:44:02", + "created_by": "linode", + "deprecated": false, + "description": "Example image description.", + "eol": "2026-07-01T04:00:00", + "expiry": null, + "id": "private/15", + "image_sharing": { + "shared_by": null, + "shared_with": { + "sharegroup_count": 0, + "sharegroup_list_url": "/images/private/15/sharegroups" + } + }, + "is_public": false, + "is_shared": false, + "label": "Debian 11", "regions": [ { - "region": "us-east", - "status": "available" - }, - { - "region": "us-central", - "status": "pending" - }] - } + "region": "us-iad", + "status": "available" + } + ], + "size": 2500, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 1234567, + "type": "manual", + "updated": "2021-08-14T22:44:02", + "vendor": "Debian" + } ] ``` - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-images) for a list of returned fields diff --git a/docs/modules/image_share_group_image_list.md b/docs/modules/image_share_group_image_list.md new file mode 100644 index 00000000..7cd80de2 --- /dev/null +++ b/docs/modules/image_share_group_image_list.md @@ -0,0 +1,93 @@ +# image_share_group_image_list + +List and filter on Image Share Group Images. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all of the Image Share Group Images for the specified Share Group + linode.cloud.image_share_group_image_list: + sharegroup_id: 123 +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `sharegroup_id` |
`int`
|
**Required**
| The parent Image Share Group for the Image Share Group Images. | +| `order` |
`str`
|
Optional
| The order to list Image Share Group Images in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order Image Share Group Images by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Group Images. | +| `count` |
`int`
|
Optional
| The number of Image Share Group Images to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-images). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `image_share_group_images` - The returned Image Share Group Images. + + - Sample Response: + ```json + [ + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2025-08-04T10:07:59", + "created_by": null, + "deprecated": true, + "description": "Official Debian Linux image for server deployment", + "eol": "2025-12-31T18:13:44.756Z", + "expiry": "2025-12-31T18:13:44.756Z", + "id": "shared/1", + "image_sharing": { + "shared_by": { + "sharegroup_id": 123, + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "8d64b99e-92f7-4c7b-a616-8f622fffb94c", + "source_image_id": "private/15" + }, + "shared_with": null + }, + "is_public": true, + "is_shared": "none", + "label": "Linux Debian", + "regions": [ + { + "region": "us-iad", + "status": "available" + } + ], + "size": 256, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 256, + "type": "shared", + "updated": null, + "vendor": "string" + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-images) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/image.py b/plugins/module_utils/doc_fragments/image.py index a6e70cb2..c5aa3e59 100644 --- a/plugins/module_utils/doc_fragments/image.py +++ b/plugins/module_utils/doc_fragments/image.py @@ -34,31 +34,41 @@ state: absent'''] result_image_samples = ['''{ - "capabilities": [], + "capabilities": [ + "cloud-init", + "distributed-sites" + ], "created": "2021-08-14T22:44:02", - "created_by": "my-account", + "created_by": "linode", "deprecated": false, - "description": "Example Image description.", + "description": "Example image description.", "eol": "2026-07-01T04:00:00", "expiry": null, - "id": "private/123", - "is_public": true, - "label": "my-image", - "size": 2500, - "status": null, - "type": "manual", - "updated": "2021-08-14T22:44:02", - "vendor": "Debian", - "tags": ["test"], - "total_size": 5000, + "id": "private/15", + "image_sharing": { + "shared_by": null, + "shared_with": { + "sharegroup_count": 0, + "sharegroup_list_url": "/images/private/15/sharegroups" + } + }, + "is_public": false, + "is_shared": false, + "label": "Debian 11", "regions": [ { - "region": "us-east", - "status": "available" - }, - { - "region": "us-central", - "status": "pending" + "region": "us-iad", + "status": "available" } - ] + ], + "size": 2500, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 1234567, + "type": "manual", + "updated": "2021-08-14T22:44:02", + "vendor": "Debian" }'''] diff --git a/plugins/module_utils/doc_fragments/image_list.py b/plugins/module_utils/doc_fragments/image_list.py index 589d39c9..37396626 100644 --- a/plugins/module_utils/doc_fragments/image_list.py +++ b/plugins/module_utils/doc_fragments/image_list.py @@ -15,31 +15,43 @@ values: Alpine'''] result_images_samples = ['''[ - { - "created":"2021-08-14T22:44:02", - "created_by":"my-account", - "deprecated":false, - "description":"Example Image description.", - "eol":"2026-07-01T04:00:00", - "expiry":null, - "id":"private/123", - "is_public":false, - "label":"test", - "size":2500, - "status":null, - "type":"manual", - "updated":"2021-08-14T22:44:02", - "vendor":"Debian", - "tags": ["test"], - "total_size": 5000, + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2021-08-14T22:44:02", + "created_by": "linode", + "deprecated": false, + "description": "Example image description.", + "eol": "2026-07-01T04:00:00", + "expiry": null, + "id": "private/15", + "image_sharing": { + "shared_by": null, + "shared_with": { + "sharegroup_count": 0, + "sharegroup_list_url": "/images/private/15/sharegroups" + } + }, + "is_public": false, + "is_shared": false, + "label": "Debian 11", "regions": [ { - "region": "us-east", - "status": "available" - }, - { - "region": "us-central", - "status": "pending" - }] - } + "region": "us-iad", + "status": "available" + } + ], + "size": 2500, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 1234567, + "type": "manual", + "updated": "2021-08-14T22:44:02", + "vendor": "Debian" + } ]'''] diff --git a/plugins/module_utils/doc_fragments/image_share_group_image_list.py b/plugins/module_utils/doc_fragments/image_share_group_image_list.py new file mode 100644 index 00000000..7fd437bc --- /dev/null +++ b/plugins/module_utils/doc_fragments/image_share_group_image_list.py @@ -0,0 +1,50 @@ +"""Documentation fragments for the image_share_group_image_list module""" + +specdoc_examples = [''' +- name: List all of the Image Share Group Images for the specified Share Group + linode.cloud.image_share_group_image_list: + sharegroup_id: 123'''] + +result_image_share_group_images_samples = ['''[ + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2025-08-04T10:07:59", + "created_by": null, + "deprecated": true, + "description": "Official Debian Linux image for server deployment", + "eol": "2025-12-31T18:13:44.756Z", + "expiry": "2025-12-31T18:13:44.756Z", + "id": "shared/1", + "image_sharing": { + "shared_by": { + "sharegroup_id": 123, + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "8d64b99e-92f7-4c7b-a616-8f622fffb94c", + "source_image_id": "private/15" + }, + "shared_with": null + }, + "is_public": true, + "is_shared": "none", + "label": "Linux Debian", + "regions": [ + { + "region": "us-iad", + "status": "available" + } + ], + "size": 256, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 256, + "type": "shared", + "updated": null, + "vendor": "string" + } +]'''] diff --git a/plugins/modules/image_share_group_image_list.py b/plugins/modules/image_share_group_image_list.py new file mode 100644 index 00000000..75b81999 --- /dev/null +++ b/plugins/modules/image_share_group_image_list.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module allows users to list Image Share Group Images.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + image_share_group_image_list as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, + ListModuleParam, +) +from ansible_specdoc.objects import FieldType + +module = ListModule( + result_display_name="Image Share Group Images", + result_field_name="image_share_group_images", + endpoint_template="/images/sharegroups/{sharegroup_id}/images", + result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-images", + result_samples=docs.result_image_share_group_images_samples, + examples=docs.specdoc_examples, + params=[ + ListModuleParam( + display_name="Image Share Group", + name="sharegroup_id", + type=FieldType.integer, + ), + ], +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/image_share_group_basic/tasks/main.yaml b/tests/integration/targets/image_share_group_basic/tasks/main.yaml index da0b2779..1b338094 100644 --- a/tests/integration/targets/image_share_group_basic/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_basic/tasks/main.yaml @@ -201,6 +201,11 @@ label: '{{ share_group_update_details.image_share_group.label }}' state: absent + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_populated_images_removed.image_share_group.label }}' + state: absent + - name: Delete image linode.cloud.image: label: '{{ image_one_create.image.label }}' diff --git a/tests/integration/targets/image_share_group_image_list/tasks/main.yaml b/tests/integration/targets/image_share_group_image_list/tasks/main.yaml new file mode 100644 index 00000000..bfbc144f --- /dev/null +++ b/tests/integration/targets/image_share_group_image_list/tasks/main.yaml @@ -0,0 +1,145 @@ +- name: image_share_group_images_list + block: + - set_fact: + r: "{{ 1000000000 | random }}" + disallowed_image_regions: ["gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3", "no-osl-1"] + + - name: List regions + linode.cloud.region_list: { } + register: all_regions + + - set_fact: + capable_regions: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Object Storage") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' + + - name: Create instance one to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-one' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_one_create + + - name: Create image one + linode.cloud.image: + disk_id: "{{ instance_one_create.disks.0.id }}" + label: image-one + description: first image + state: present + register: image_one_create + + - name: Create instance two to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-two' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_two_create + + - name: Create image two + linode.cloud.image: + disk_id: "{{ instance_two_create.disks.0.id }}" + label: image-two + description: second-image + state: present + register: image_two_create + + - name: Assert images are created + assert: + that: + - image_one_create.image.status == 'available' + - image_two_create.image.status == 'available' + - image_one_create.image.image_sharing.shared_with.sharegroup_count == 0 + - image_two_create.image.image_sharing.shared_with.sharegroup_count == 0 + + - name: Create an image share group + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + images: + - id: '{{ image_one_create.image.id }}' + label: 'image-one' + description: 'first image' + - id: '{{ image_two_create.image.id }}' + label: 'image-two' + description: 'second image' + state: present + register: share_group_create + + - name: List image share group images with no filter + linode.cloud.image_share_group_image_list: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + count: 2 + register: no_filter + + - name: Assert image_share_group_image_list with no filter + assert: + that: + - no_filter.image_share_group_images | length == 2 + + - name: List image share group images with filter on label + linode.cloud.image_share_group_image_list: + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + count: 1 + order_by: created + order: desc + filters: + - name: label + values: 'image-one' + register: filter + + - name: Assert image_share_group_image_list with filter on label + assert: + that: + - filter.image_share_group_images | length == 1 + - filter.image_share_group_images[0].label == "image-one" + + - name: Remove remaining images in share group + linode.cloud.image_share_group: + label: '{{ share_group_create.image_share_group.label }}' + images: [] + state: present + register: share_group_images_removed + + - name: Assert images removed from share group + assert: + that: + - share_group_images_removed.image_share_group.images | length == 0 + - share_group_images_removed.image_share_group.images_count == 0 + + always: + - ignore_errors: yes + block: + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_images_removed.image_share_group.label }}' + state: absent + + - name: Delete image + linode.cloud.image: + label: '{{ image_one_create.image.label }}' + state: absent + + - name: Delete image + linode.cloud.image: + label: '{{ image_two_create.image.label }}' + state: absent + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_one_create.instance.label }}' + state: absent + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_two_create.instance.label }}' + state: absent + + environment: + LINODE_UA_PREFIX: '{{ ua_prefix }}' + LINODE_API_TOKEN: '{{ api_token }}' + LINODE_API_URL: '{{ api_url }}' + LINODE_API_VERSION: '{{ api_version }}' + LINODE_CA: '{{ ca_file or "" }}' From 007a8536715e341ddf2d0d32e6ad4fbe914e6aa7 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 14:30:18 -0500 Subject: [PATCH 08/14] Added Consumer Image Share Group Info module --- README.md | 1 + .../consumer_image_share_group_info.md | 48 ++++++++++ .../consumer_image_share_group_info.py | 16 ++++ .../consumer_image_share_group_info.py | 54 +++++++++++ .../tasks/main.yaml | 94 +++++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 docs/modules/consumer_image_share_group_info.md create mode 100644 plugins/module_utils/doc_fragments/consumer_image_share_group_info.py create mode 100644 plugins/modules/consumer_image_share_group_info.py create mode 100644 tests/integration/targets/consumer_image_share_group_info/tasks/main.yaml diff --git a/README.md b/README.md index 6326ed21..68380336 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Name | Description | [linode.cloud.account_availability_info](./docs/modules/account_availability_info.md)|Get info about a Linode Account Availability.| [linode.cloud.account_info](./docs/modules/account_info.md)|Get info about a Linode Account.| [linode.cloud.child_account_info](./docs/modules/child_account_info.md)|Get info about a Linode Child Account.| +[linode.cloud.consumer_image_share_group_info](./docs/modules/consumer_image_share_group_info.md)|Get info about a Linode Image Share Group.| [linode.cloud.database_config_info](./docs/modules/database_config_info.md)|Get info about a Linode Configuration.| [linode.cloud.database_mysql_info](./docs/modules/database_mysql_info.md)|Get info about a Linode MySQL Managed Database.| [linode.cloud.database_postgresql_info](./docs/modules/database_postgresql_info.md)|Get info about a Linode PostgreSQL Managed Database.| diff --git a/docs/modules/consumer_image_share_group_info.md b/docs/modules/consumer_image_share_group_info.md new file mode 100644 index 00000000..404e6531 --- /dev/null +++ b/docs/modules/consumer_image_share_group_info.md @@ -0,0 +1,48 @@ +# consumer_image_share_group_info + +Get info about a Linode Image Share Group. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: Get info about an Image Share Group by a Consumer's Token UUID + linode.cloud.consumer_image_share_group_info: + token_uuid: "1433863e-16a4-47b5-b829-ac0f35c13278" +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `token_uuid` |
`str`
|
**Required**
| The ID of the Token for this resource. | + +## Return Values + +- `image_share_group` - The returned Image Share Group. + + - Sample Response: + ```json + { + "created": "2025-04-14T22:44:02", + "description": "Group of base operating system images and engineers used for CI/CD pipelines and infrastructure automation", + "id": 1, + "is_suspended": false, + "label": "DevOps Base Images", + "updated": "2025-04-14T22:44:03", + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" + } + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-by-token) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/consumer_image_share_group_info.py b/plugins/module_utils/doc_fragments/consumer_image_share_group_info.py new file mode 100644 index 00000000..10e2eb64 --- /dev/null +++ b/plugins/module_utils/doc_fragments/consumer_image_share_group_info.py @@ -0,0 +1,16 @@ +"""Documentation fragments for the image_share_group_info module""" + +specdoc_examples = [''' +- name: Get info about an Image Share Group by a Consumer's Token UUID + linode.cloud.consumer_image_share_group_info: + token_uuid: "1433863e-16a4-47b5-b829-ac0f35c13278"'''] + +result_consumer_image_share_group_samples = ['''{ + "created": "2025-04-14T22:44:02", + "description": "Group of base operating system images and engineers used for CI/CD pipelines and infrastructure automation", + "id": 1, + "is_suspended": false, + "label": "DevOps Base Images", + "updated": "2025-04-14T22:44:03", + "uuid": "1533863e-16a4-47b5-b829-ac0f35c13278" +}'''] diff --git a/plugins/modules/consumer_image_share_group_info.py b/plugins/modules/consumer_image_share_group_info.py new file mode 100644 index 00000000..962d5602 --- /dev/null +++ b/plugins/modules/consumer_image_share_group_info.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module contains the functionality for the Consumer Image Share Group info module. +This module allows a consumer to retrieve information about the Image Share Group a specific +Image Share Group Token gives access to.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + consumer_image_share_group_info as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_info import ( + InfoModule, + InfoModuleParam, + InfoModuleResult, +) +from ansible_specdoc.objects import FieldType +from linode_api4 import ImageShareGroupToken + +module = InfoModule( + primary_result=InfoModuleResult( + field_name="image_share_group", + field_type=FieldType.dict, + display_name="Image Share Group", + docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroup-by-token", + samples=docs.result_consumer_image_share_group_samples, + get=lambda client, params, _current=None: ( + client.load(ImageShareGroupToken, params["token_uuid"]) + .get_sharegroup() + .__dict__ + ), + ), + params=[ + InfoModuleParam( + display_name="Token", + name="token_uuid", + type=FieldType.string, + ) + ], + examples=docs.specdoc_examples, +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/consumer_image_share_group_info/tasks/main.yaml b/tests/integration/targets/consumer_image_share_group_info/tasks/main.yaml new file mode 100644 index 00000000..b05e0c16 --- /dev/null +++ b/tests/integration/targets/consumer_image_share_group_info/tasks/main.yaml @@ -0,0 +1,94 @@ +- name: consumer_image_share_group_info + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + token: "{{ share_group_token_create.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Get image share group info (consumer task) + linode.cloud.consumer_image_share_group_info: + token_uuid: "{{ share_group_token_create.image_share_group_token.token_uuid }}" + register: consumer_share_group_info + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert consumer image share group info is correct + assert: + that: + - consumer_share_group_info.image_share_group.label == "ansible-test-" ~ r + + always: + - ignore_errors: yes + block: + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file From 663bfe8c1e5efe9555e481205932fa0b2a1ba7f7 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 15:33:50 -0500 Subject: [PATCH 09/14] Added Consumer Image Share Group Image List module --- README.md | 1 + .../consumer_image_share_group_image_list.md | 93 ++++++++ .../consumer_image_share_group_image_list.py | 50 ++++ .../consumer_image_share_group_image_list.py | 47 ++++ .../tasks/main.yaml | 218 ++++++++++++++++++ 5 files changed, 409 insertions(+) create mode 100644 docs/modules/consumer_image_share_group_image_list.md create mode 100644 plugins/module_utils/doc_fragments/consumer_image_share_group_image_list.py create mode 100644 plugins/modules/consumer_image_share_group_image_list.py create mode 100644 tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml diff --git a/README.md b/README.md index 68380336..e6cdcf9b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Name | Description | --- | ------------ | [linode.cloud.account_availability_list](./docs/modules/account_availability_list.md)|List and filter on Account Availabilities.| [linode.cloud.child_account_list](./docs/modules/child_account_list.md)|List and filter on Child Account.| +[linode.cloud.consumer_image_share_group_image_list](./docs/modules/consumer_image_share_group_image_list.md)|List and filter on Image Share Group Images.| [linode.cloud.database_engine_list](./docs/modules/database_engine_list.md)|List and filter on Managed Database engine types.| [linode.cloud.database_list](./docs/modules/database_list.md)|List and filter on Linode Managed Databases.| [linode.cloud.domain_list](./docs/modules/domain_list.md)|List and filter on Domains.| diff --git a/docs/modules/consumer_image_share_group_image_list.md b/docs/modules/consumer_image_share_group_image_list.md new file mode 100644 index 00000000..565763b0 --- /dev/null +++ b/docs/modules/consumer_image_share_group_image_list.md @@ -0,0 +1,93 @@ +# consumer_image_share_group_image_list + +List and filter on Image Share Group Images. + +- [Minimum Required Fields](#minimum-required-fields) +- [Examples](#examples) +- [Parameters](#parameters) +- [Return Values](#return-values) + +## Minimum Required Fields +| Field | Type | Required | Description | +|-------------|-------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_token` | `str` | **Required** | The Linode account personal access token. It is necessary to run the module.
It can be exposed by the environment variable `LINODE_API_TOKEN` instead.
See details in [Usage](https://github.com/linode/ansible_linode?tab=readme-ov-file#usage). | + +## Examples + +```yaml +- name: List all of the Image Share Group Images for the specified Token UUID + linode.cloud.consumer_image_share_group_image_list: + token_uuid: "9e64b99e-92f7-4c7b-a616-8f622fffb94c" +``` + + +## Parameters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `token_uuid` |
`str`
|
**Required**
| The parent Token for the Image Share Group Images. | +| `order` |
`str`
|
Optional
| The order to list Image Share Group Images in. **(Choices: `desc`, `asc`; Default: `asc`)** | +| `order_by` |
`str`
|
Optional
| The attribute to order Image Share Group Images by. | +| [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Group Images. | +| `count` |
`int`
|
Optional
| The number of Image Share Group Images to return. If undefined, all results will be returned. | + +### filters + +| Field | Type | Required | Description | +|-----------|------|----------|------------------------------------------------------------------------------| +| `name` |
`str`
|
**Required**
| The name of the field to filter on. Valid filterable fields can be found [here](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-images-by-token). | +| `values` |
`list`
|
**Required**
| A list of values to allow for this field. Fields will pass this filter if at least one of these values matches. | + +## Return Values + +- `image_share_group_images` - The returned Image Share Group Images. + + - Sample Response: + ```json + [ + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2025-08-04T10:07:59", + "created_by": null, + "deprecated": true, + "description": "Official Debian Linux image for server deployment", + "eol": "2025-12-31T18:13:44.756Z", + "expiry": "2025-12-31T18:13:44.756Z", + "id": "shared/1", + "image_sharing": { + "shared_by": { + "sharegroup_id": 123, + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "8d64b99e-92f7-4c7b-a616-8f622fffb94c", + "source_image_id": "private/15" + }, + "shared_with": null + }, + "is_public": true, + "is_shared": "none", + "label": "Linux Debian", + "regions": [ + { + "region": "us-iad", + "status": "available" + } + ], + "size": 256, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 256, + "type": "shared", + "updated": null, + "vendor": "string" + } + ] + ``` + - See the [Linode API response documentation](https://techdocs.akamai.com/linode-api/reference/get-sharegroup-images-by-token) for a list of returned fields + + diff --git a/plugins/module_utils/doc_fragments/consumer_image_share_group_image_list.py b/plugins/module_utils/doc_fragments/consumer_image_share_group_image_list.py new file mode 100644 index 00000000..0893664c --- /dev/null +++ b/plugins/module_utils/doc_fragments/consumer_image_share_group_image_list.py @@ -0,0 +1,50 @@ +"""Documentation fragments for the consumer_image_share_group_image_list module""" + +specdoc_examples = [''' +- name: List all of the Image Share Group Images for the specified Token UUID + linode.cloud.consumer_image_share_group_image_list: + token_uuid: "9e64b99e-92f7-4c7b-a616-8f622fffb94c"'''] + +result_consumer_image_share_group_images_samples = ['''[ + { + "capabilities": [ + "cloud-init", + "distributed-sites" + ], + "created": "2025-08-04T10:07:59", + "created_by": null, + "deprecated": true, + "description": "Official Debian Linux image for server deployment", + "eol": "2025-12-31T18:13:44.756Z", + "expiry": "2025-12-31T18:13:44.756Z", + "id": "shared/1", + "image_sharing": { + "shared_by": { + "sharegroup_id": 123, + "sharegroup_label": "DevOps Base Images", + "sharegroup_uuid": "8d64b99e-92f7-4c7b-a616-8f622fffb94c", + "source_image_id": "private/15" + }, + "shared_with": null + }, + "is_public": true, + "is_shared": "none", + "label": "Linux Debian", + "regions": [ + { + "region": "us-iad", + "status": "available" + } + ], + "size": 256, + "status": "available", + "tags": [ + "repair-image", + "fix-1" + ], + "total_size": 256, + "type": "shared", + "updated": null, + "vendor": "string" + } +]'''] diff --git a/plugins/modules/consumer_image_share_group_image_list.py b/plugins/modules/consumer_image_share_group_image_list.py new file mode 100644 index 00000000..38ecee46 --- /dev/null +++ b/plugins/modules/consumer_image_share_group_image_list.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +"""This module allows Consumers to list the Images in an Image Share Group a specific +Image Share Group Token gives access to.""" + +from __future__ import absolute_import, division, print_function + +from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( + consumer_image_share_group_image_list as docs, +) +from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( + ListModule, + ListModuleParam, +) +from ansible_specdoc.objects import FieldType + +module = ListModule( + result_display_name="Image Share Group Images", + result_field_name="image_share_group_images", + endpoint_template="/images/sharegroups/tokens/{token_uuid}/sharegroup/images", + result_docs_url=( + "https://techdocs.akamai.com/linode-api/reference/" + "get-sharegroup-images-by-token" + ), + result_samples=docs.result_consumer_image_share_group_images_samples, + examples=docs.specdoc_examples, + params=[ + ListModuleParam( + display_name="Token", + name="token_uuid", + type=FieldType.string, + ), + ], +) + +SPECDOC_META = module.spec + +DOCUMENTATION = r""" +""" +EXAMPLES = r""" +""" +RETURN = r""" +""" + +if __name__ == "__main__": + module.run() diff --git a/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml b/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml new file mode 100644 index 00000000..f1ebca74 --- /dev/null +++ b/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml @@ -0,0 +1,218 @@ +- name: consumer_image_share_group_images_list + block: + - name: Normalize producer/consumer tokens + set_fact: + _producer_token: "{{ producer_api_token | default('', true) | trim }}" + _consumer_token: "{{ consumer_api_token | default('', true) | trim }}" + + - name: Skip test if producer/consumer tokens are missing + meta: end_play + when: _producer_token == '' or _consumer_token == '' + + - set_fact: + r: "{{ 1000000000 | random }}" + disallowed_image_regions: ["gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3", "no-osl-1"] + + - name: List regions + linode.cloud.region_list: { } + register: all_regions + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - set_fact: + capable_regions: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Object Storage") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' + + - name: Create instance one to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-one' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_one_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image one + linode.cloud.image: + disk_id: "{{ instance_one_create.disks.0.id }}" + label: image-one + description: first image + state: present + register: image_one_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create instance two to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}-two' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_two_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image two + linode.cloud.image: + disk_id: "{{ instance_two_create.disks.0.id }}" + label: image-two + description: second-image + state: present + register: image_two_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group (producer task) + linode.cloud.image_share_group: + label: "ansible-test-{{ r }}" + images: + - id: '{{ image_one_create.image.id }}' + label: 'image-one' + description: 'first image' + - id: '{{ image_two_create.image.id }}' + label: 'image-two' + description: 'second image' + state: present + register: share_group_create + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Create image share group token (consumer task) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}" + state: present + register: share_group_token_create + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Add consumer as member to share group (producer task) + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + token: "{{ share_group_token_create.single_use_token }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: present + register: share_group_member_add + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: List image share group images with no filter + linode.cloud.consumer_image_share_group_image_list: + token_uuid: "{{ share_group_token_create.image_share_group_token.token_uuid }}" + count: 2 + register: no_filter + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert consumer_image_share_group_image_list with no filter + assert: + that: + - no_filter.image_share_group_images | length == 2 + + - name: List image share group images with filter on label + linode.cloud.consumer_image_share_group_image_list: + token_uuid: "{{ share_group_token_create.image_share_group_token.token_uuid }}" + count: 1 + order_by: created + order: desc + filters: + - name: label + values: 'image-one' + register: filter + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert consumer_image_share_group_image_list with filter on label + assert: + that: + - filter.image_share_group_images | length == 1 + - filter.image_share_group_images[0].label == "image-one" + + - name: Remove remaining images in share group + linode.cloud.image_share_group: + label: '{{ share_group_create.image_share_group.label }}' + images: [ ] + state: present + register: share_group_images_removed + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Assert images removed from share group + assert: + that: + - share_group_images_removed.image_share_group.images | length == 0 + - share_group_images_removed.image_share_group.images_count == 0 + + always: + - ignore_errors: yes + block: + - name: Delete image share group member + linode.cloud.image_share_group_member: + label: "ansible-test-{{ r }}" + sharegroup_id: "{{ share_group_create.image_share_group.id }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image share group token + linode.cloud.image_share_group_token: + label: "{{ share_group_token_create.image_share_group_token.label }}" + state: absent + when: + - share_group_token_create is defined + - share_group_token_create.image_share_group_token is defined + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Delete image share group + linode.cloud.image_share_group: + label: "{{ share_group_create.image_share_group.label }}" + state: absent + when: + - share_group_create is defined + - share_group_create.image_share_group is defined + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image + linode.cloud.image: + label: '{{ image_one_create.image.label }}' + state: absent + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete image + linode.cloud.image: + label: '{{ image_two_create.image.label }}' + state: absent + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_one_create.instance.label }}' + state: absent + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_two_create.instance.label }}' + state: absent + environment: + LINODE_API_TOKEN: "{{ producer_api_token }}" + + environment: + LINODE_UA_PREFIX: "{{ ua_prefix }}" + LINODE_PRODUCER_API_TOKEN: "{{ producer_api_token }}" + LINODE_CONSUMER_API_TOKEN: "{{ consumer_api_token }}" + LINODE_API_URL: "{{ api_url }}" + LINODE_API_VERSION: "{{ api_version }}" + LINODE_CA: "{{ ca_file or '' }}" \ No newline at end of file From 30d580557b3400c5d9ecdc37b1408e69191df58b Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Wed, 31 Dec 2025 15:38:57 -0500 Subject: [PATCH 10/14] Address copilot suggestions --- .../targets/image_share_group_member_list/tasks/main.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/image_share_group_member_list/tasks/main.yaml b/tests/integration/targets/image_share_group_member_list/tasks/main.yaml index 8ddbb859..cd2166ad 100644 --- a/tests/integration/targets/image_share_group_member_list/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_member_list/tasks/main.yaml @@ -120,8 +120,8 @@ label: "{{ share_group_token_create_one.image_share_group_token.label }}" state: absent when: - - share_group_token_create is defined - - share_group_token_create.image_share_group_token is defined + - share_group_token_create_one is defined + - share_group_token_create_one.image_share_group_token is defined environment: LINODE_API_TOKEN: "{{ consumer_api_token }}" @@ -130,8 +130,8 @@ label: "{{ share_group_token_create_two.image_share_group_token.label }}" state: absent when: - - share_group_token_create is defined - - share_group_token_create.image_share_group_token is defined + - share_group_token_create_two is defined + - share_group_token_create_two.image_share_group_token is defined environment: LINODE_API_TOKEN: "{{ consumer_api_token }}" From 3b51389bbbb25a1c1aac23919e61dc4a844a16db Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 2 Jan 2026 09:59:22 -0500 Subject: [PATCH 11/14] Added support for list sharegroups by image endpoint --- docs/modules/image_share_group_list.md | 7 ++ .../doc_fragments/image_share_group_list.py | 5 +- plugins/modules/image_share_group_list.py | 32 ++++++ .../image_share_group_list/tasks/main.yaml | 98 ++++++++++++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/docs/modules/image_share_group_list.md b/docs/modules/image_share_group_list.md index 683ba06b..62f9dbd2 100644 --- a/docs/modules/image_share_group_list.md +++ b/docs/modules/image_share_group_list.md @@ -19,6 +19,12 @@ List and filter on Image Share Groups. linode.cloud.image_share_group_list: {} ``` +```yaml +- name: List all of the Image Share Groups that contain a specific private image + linode.cloud.image_share_group_list: + image_id: "private/12345" +``` + ## Parameters @@ -28,6 +34,7 @@ List and filter on Image Share Groups. | `order_by` |
`str`
|
Optional
| The attribute to order Image Share Groups by. | | [`filters` (sub-options)](#filters) |
`list`
|
Optional
| A list of filters to apply to the resulting Image Share Groups. | | `count` |
`int`
|
Optional
| The number of Image Share Groups to return. If undefined, all results will be returned. | +| `image_id` |
`str`
|
Optional
| Specifies the private image ID to list share groups for. If provided, only share groups containing the specified image will be returned. | ### filters diff --git a/plugins/module_utils/doc_fragments/image_share_group_list.py b/plugins/module_utils/doc_fragments/image_share_group_list.py index 40ae8818..271ca492 100644 --- a/plugins/module_utils/doc_fragments/image_share_group_list.py +++ b/plugins/module_utils/doc_fragments/image_share_group_list.py @@ -2,7 +2,10 @@ specdoc_examples = [''' - name: List all of the Image Share Groups for the current Linode Account - linode.cloud.image_share_group_list: {}'''] + linode.cloud.image_share_group_list: {}''', ''' +- name: List all of the Image Share Groups that contain a specific private image + linode.cloud.image_share_group_list: + image_id: "private/12345"'''] result_image_share_groups_samples = ['''[ { diff --git a/plugins/modules/image_share_group_list.py b/plugins/modules/image_share_group_list.py index c10db6f3..e55230e0 100644 --- a/plugins/modules/image_share_group_list.py +++ b/plugins/modules/image_share_group_list.py @@ -5,12 +5,33 @@ from __future__ import absolute_import, division, print_function +from typing import Dict + from ansible_collections.linode.cloud.plugins.module_utils.doc_fragments import ( image_share_group_list as docs, ) from ansible_collections.linode.cloud.plugins.module_utils.linode_common_list import ( ListModule, ) +from ansible_specdoc.objects import FieldType, SpecField + + +def custom_field_resolver(params: Dict[str, str]) -> Dict[str, str]: + """ + Resolves the appropriate endpoint based on the 'image_id' parameter. + + :param params: The parameters passed to the module. + + :returns: The appropriate documentation and examples. + """ + if params.get("image_id"): + return { + "endpoint_template": f"/images/{params.get('image_id')}/sharegroups", + } + return { + "endpoint_template": "/images/sharegroups", + } + module = ListModule( result_display_name="Image Share Groups", @@ -19,6 +40,17 @@ result_docs_url="https://techdocs.akamai.com/linode-api/reference/get-sharegroups", result_samples=docs.result_image_share_groups_samples, examples=docs.specdoc_examples, + custom_options={ + "image_id": SpecField( + type=FieldType.string, + description=[ + "Specifies the private image ID to list share groups for.", + "If provided, only share groups containing the specified image will be returned.", + ], + required=False, + ), + }, + custom_field_resolver=custom_field_resolver, ) SPECDOC_META = module.spec diff --git a/tests/integration/targets/image_share_group_list/tasks/main.yaml b/tests/integration/targets/image_share_group_list/tasks/main.yaml index 62c9105a..dfaf93b1 100644 --- a/tests/integration/targets/image_share_group_list/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_list/tasks/main.yaml @@ -2,28 +2,63 @@ block: - set_fact: r: "{{ 1000000000 | random }}" + disallowed_image_regions: [ "gb-lon", "au-mel", "sg-sin-2", "jp-tyo-3", "no-osl-1" ] + + - name: List regions + linode.cloud.region_list: { } + register: all_regions + + - set_fact: + capable_regions: '{{ ( all_regions.regions | selectattr("site_type", "equalto", "core") | selectattr("capabilities", "search", "Object Storage") | rejectattr("id", "in", disallowed_image_regions) | map(attribute="id") | list) }}' + + - name: Create an instance to image + linode.cloud.instance: + label: 'ansible-test-{{ r }}' + region: '{{ capable_regions[1] }}' + type: g6-standard-1 + image: linode/alpine3.19 + state: present + firewall_id: '{{ firewall_id }}' + register: instance_create + + - name: Create an image + linode.cloud.image: + disk_id: "{{ instance_create.disks.0.id }}" + label: 'ansible-test-{{ r }}' + state: present + register: image_create - name: Create an image share group linode.cloud.image_share_group: label: 'ansible-test-{{ r }}-one' + images: + - id: '{{ image_create.image.id }}' state: present register: share_group_one_create - name: Create another image share group linode.cloud.image_share_group: label: 'ansible-test-{{ r }}-two' + images: + - id: '{{ image_create.image.id }}' state: present register: share_group_two_create + - name: Create another image share group + linode.cloud.image_share_group: + label: 'ansible-test-{{ r }}-three' + state: present + register: share_group_three_create + - name: List Image Share Groups with no filter linode.cloud.image_share_group_list: - count: 2 + count: 3 register: no_filter - name: Assert image_share_group_list with no filter assert: that: - - no_filter.image_share_groups | length == 2 + - no_filter.image_share_groups | length == 3 - name: List Image Share Groups with filter on label linode.cloud.image_share_group_list: @@ -41,9 +76,63 @@ - filter.image_share_groups | length == 1 - filter.image_share_groups[0].label == "ansible-test-" ~ r ~ "-one" + - name: List Image Share Groups by Image ID with no filter + linode.cloud.image_share_group_list: + count: 2 + order_by: created + order: desc + image_id: '{{ image_create.image.id }}' + register: by_image_id_no_filter + + - name: Assert image_share_group_list by Image ID with no filter + assert: + that: + - by_image_id_no_filter.image_share_groups | length == 2 + + - name: List Image Share Groups by Image ID with filter on label + linode.cloud.image_share_group_list: + count: 1 + order_by: created + order: desc + image_id: '{{ image_create.image.id }}' + filters: + - name: label + values: 'ansible-test-{{ r }}-one' + register: by_image_id_filter + + - name: Assert image_share_group_list by Image ID with filter on label + assert: + that: + - by_image_id_filter.image_share_groups | length == 1 + - by_image_id_filter.image_share_groups[0].label == "ansible-test-" ~ r ~ "-one" + + - name: Remove remaining images in share group one + linode.cloud.image_share_group: + label: '{{ share_group_one_create.image_share_group.label }}' + images: [] + state: present + register: share_group_one_empty + + - name: Remove remaining images in share group two + linode.cloud.image_share_group: + label: '{{ share_group_two_create.image_share_group.label }}' + images: [ ] + state: present + register: share_group_two_empty + always: - ignore_errors: yes block: + - name: Delete image + linode.cloud.image: + label: '{{ image_create.image.label }}' + state: absent + + - name: Delete instance + linode.cloud.instance: + label: '{{ instance_create.instance.label }}' + state: absent + - name: Delete image share group linode.cloud.image_share_group: label: '{{ share_group_one_create.image_share_group.label }}' @@ -54,6 +143,11 @@ label: '{{ share_group_two_create.image_share_group.label }}' state: absent + - name: Delete image share group + linode.cloud.image_share_group: + label: '{{ share_group_three_create.image_share_group.label }}' + state: absent + environment: LINODE_UA_PREFIX: '{{ ua_prefix }}' LINODE_API_TOKEN: '{{ api_token }}' From d199851d75a03d0397ec99d70d423bd30d6f6fb2 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 2 Jan 2026 10:04:56 -0500 Subject: [PATCH 12/14] Addressed more CoPilot suggestions --- .../consumer_image_share_group_image_list/tasks/main.yaml | 2 +- .../targets/image_share_group_image_list/tasks/main.yaml | 2 +- .../targets/image_share_group_member_info/tasks/main.yaml | 2 +- .../targets/image_share_group_token_list/tasks/main.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml b/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml index f1ebca74..4a21e097 100644 --- a/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml +++ b/tests/integration/targets/consumer_image_share_group_image_list/tasks/main.yaml @@ -1,4 +1,4 @@ -- name: consumer_image_share_group_images_list +- name: consumer_image_share_group_image_list block: - name: Normalize producer/consumer tokens set_fact: diff --git a/tests/integration/targets/image_share_group_image_list/tasks/main.yaml b/tests/integration/targets/image_share_group_image_list/tasks/main.yaml index bfbc144f..4572cb50 100644 --- a/tests/integration/targets/image_share_group_image_list/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_image_list/tasks/main.yaml @@ -1,4 +1,4 @@ -- name: image_share_group_images_list +- name: image_share_group_image_list block: - set_fact: r: "{{ 1000000000 | random }}" diff --git a/tests/integration/targets/image_share_group_member_info/tasks/main.yaml b/tests/integration/targets/image_share_group_member_info/tasks/main.yaml index c7a96d1e..ddad9ff0 100644 --- a/tests/integration/targets/image_share_group_member_info/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_member_info/tasks/main.yaml @@ -1,4 +1,4 @@ -- name: image_share_group_member_list +- name: image_share_group_member_info block: - name: Normalize producer/consumer tokens set_fact: diff --git a/tests/integration/targets/image_share_group_token_list/tasks/main.yaml b/tests/integration/targets/image_share_group_token_list/tasks/main.yaml index 900b37b8..416e5066 100644 --- a/tests/integration/targets/image_share_group_token_list/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_token_list/tasks/main.yaml @@ -1,4 +1,4 @@ -- name: image_share_group_token_info +- name: image_share_group_token_list block: - name: Normalize producer/consumer tokens set_fact: From 1b8c85f5ae9d9277dff25fed1ce2486a7c61bae4 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Thu, 8 Jan 2026 09:30:54 -0500 Subject: [PATCH 13/14] Addressed PR comments --- plugins/modules/image_share_group_token.py | 20 +++++++++++++++---- .../image_share_group_token/tasks/main.yaml | 16 +++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/plugins/modules/image_share_group_token.py b/plugins/modules/image_share_group_token.py index ae3535d0..5a76727c 100644 --- a/plugins/modules/image_share_group_token.py +++ b/plugins/modules/image_share_group_token.py @@ -138,16 +138,28 @@ def _create(self) -> Optional[ImageShareGroupToken]: def _handle_present(self) -> None: label = self.module.params.get("label") + requested_uuid = self.module.params.get("valid_for_sharegroup_uuid") + token = self._get_image_share_group_token_by_label(label) - if not token: + if token: + token._api_get() + + existing_uuid = token.valid_for_sharegroup_uuid + + if requested_uuid != existing_uuid: + self.fail( + msg=( + "failed to update {} -> {}: valid_for_sharegroup_uuid " + "is a non-updatable field" + ).format(existing_uuid, requested_uuid) + ) + else: token = self._create() self.register_action( "Created Image Share Group Token {0}".format(label) ) - - # Force lazy-loading - token._api_get() + token._api_get() self.results["image_share_group_token"] = token._raw_json diff --git a/tests/integration/targets/image_share_group_token/tasks/main.yaml b/tests/integration/targets/image_share_group_token/tasks/main.yaml index 32ef1a9e..07bad84b 100644 --- a/tests/integration/targets/image_share_group_token/tasks/main.yaml +++ b/tests/integration/targets/image_share_group_token/tasks/main.yaml @@ -44,6 +44,22 @@ - share_group_token_create.single_use_token is defined - share_group_token_create.single_use_token | length > 0 + - name: Attempt to update image share group token with modified share group UUID (should fail) + linode.cloud.image_share_group_token: + label: "ansible-test-{{ r }}" + valid_for_sharegroup_uuid: "{{ share_group_create.image_share_group.uuid }}x" + state: present + register: share_group_token_update_attempt + failed_when: false + environment: + LINODE_API_TOKEN: "{{ consumer_api_token }}" + + - name: Assert image share group token update fails for non-mutable field + assert: + that: + - share_group_token_update_attempt.msg is defined + - "'non-updatable field' in share_group_token_update_attempt.msg" + always: - ignore_errors: yes block: From 5252f32b8bb24d2fc235cb88f54f5323520f3e76 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 16 Jan 2026 13:56:07 -0500 Subject: [PATCH 14/14] Point at latest Python SDK release --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8182a627..119a88e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -#linode-api4>=5.38.0 -git+https://github.com/linode/linode_api4-python.git@ce04f894279cd01ad1d2bc82b223cec4cadb5253#egg=linode-api4 +linode-api4>=5.39.0 polling==0.3.2 ansible-specdoc>=0.0.19