From 5494d153b1345b48bd42206ade38d29c005d6540 Mon Sep 17 00:00:00 2001 From: Callum Dickinson Date: Wed, 3 Jul 2024 12:16:04 +1200 Subject: [PATCH] Add the object_containers_info module This adds a module for getting information on one or more object storage containers from OpenStack. The following options are supported: * name - Get details for a single container by name. When this parameter is defined a single container is returned, with extra metadata available (with the same keys and value as in the existing object_container module). * prefix - Search for and return a list of containers by prefix. When searching for containers, only a subset of metadata values are available. When no options are specified, all containers in the project are returned, with the same metadata available as when the prefix option is used. Change-Id: I8ba434a86050f72d8ce85c9e98731f6ef552fc79 --- .../object_containers_info/defaults/main.yml | 37 ++++ .../object_containers_info/tasks/main.yml | 124 +++++++++++ ci/run-collection.yml | 1 + meta/runtime.yml | 1 + plugins/modules/object_containers_info.py | 202 ++++++++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 ci/roles/object_containers_info/defaults/main.yml create mode 100644 ci/roles/object_containers_info/tasks/main.yml create mode 100644 plugins/modules/object_containers_info.py diff --git a/ci/roles/object_containers_info/defaults/main.yml b/ci/roles/object_containers_info/defaults/main.yml new file mode 100644 index 00000000..6dec7bac --- /dev/null +++ b/ci/roles/object_containers_info/defaults/main.yml @@ -0,0 +1,37 @@ +--- + +test_container_unprefixed_name: ansible-test-container +test_container_prefixed_prefix: ansible-prefixed-test-container +test_container_prefixed_num: 2 + +test_object_data: "Hello, world!" + +expected_fields_single: + - bytes + - bytes_used + - content_type + - count + - history_location + - id + - if_none_match + - is_content_type_detected + - is_newest + - meta_temp_url_key + - meta_temp_url_key_2 + - name + - object_count + - read_ACL + - storage_policy + - sync_key + - sync_to + - timestamp + - versions_location + - write_ACL + +expected_fields_multiple: + - bytes + - bytes_used + - count + - id + - name + - object_count diff --git a/ci/roles/object_containers_info/tasks/main.yml b/ci/roles/object_containers_info/tasks/main.yml new file mode 100644 index 00000000..06df290d --- /dev/null +++ b/ci/roles/object_containers_info/tasks/main.yml @@ -0,0 +1,124 @@ +--- + +- name: Generate list of containers to create + ansible.builtin.set_fact: + all_test_containers: >- + {{ + [test_container_unprefixed_name] + + ( + [test_container_prefixed_prefix + '-'] + | product(range(test_container_prefixed_num) | map('string')) + | map('join', '') + ) + }} + +- name: Run checks + block: + + - name: Create all containers + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: "{{ item }}" + read_ACL: ".r:*,.rlistings" + loop: "{{ all_test_containers }}" + + - name: Create an object in all containers + openstack.cloud.object: + cloud: "{{ cloud }}" + container: "{{ item }}" + name: hello.txt + data: "{{ test_object_data }}" + loop: "{{ all_test_containers }}" + + - name: Fetch single containers by name + openstack.cloud.object_containers_info: + cloud: "{{ cloud }}" + name: "{{ item }}" + register: single_containers + loop: "{{ all_test_containers }}" + + - name: Check that all fields are returned for single containers + ansible.builtin.assert: + that: + - (item.containers | length) == 1 + - item.containers[0].name == item.item + - item.containers[0].bytes == (test_object_data | length) + - item.containers[0].read_ACL == ".r:*,.rlistings" + # allow new fields to be introduced but prevent fields from being removed + - (expected_fields_single | difference(item.containers[0].keys()) | length) == 0 + quiet: true + loop: "{{ single_containers.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Fetch multiple containers by prefix + openstack.cloud.object_containers_info: + cloud: "{{ cloud }}" + prefix: "{{ test_container_prefixed_prefix }}" + register: multiple_containers + + - name: Check that the correct number of prefixed containers were returned + ansible.builtin.assert: + that: + - (multiple_containers.containers | length) == test_container_prefixed_num + fail_msg: >- + Incorrect number of containers found + (found {{ multiple_containers.containers | length }}, + expected {{ test_container_prefixed_num }}) + quiet: true + + - name: Check that all prefixed containers exist + ansible.builtin.assert: + that: + - >- + (test_container_prefixed_prefix + '-' + (item | string)) + in (multiple_containers.containers | map(attribute='name')) + fail_msg: "Container not found: {{ test_container_prefixed_prefix + '-' + (item | string) }}" + quiet: true + loop: "{{ range(test_container_prefixed_num) | list }}" + loop_control: + label: "{{ test_container_prefixed_prefix + '-' + (item | string) }}" + + - name: Check that the expected fields are returned for all prefixed containers + ansible.builtin.assert: + that: + - item.name.startswith(test_container_prefixed_prefix) + # allow new fields to be introduced but prevent fields from being removed + - (expected_fields_multiple | difference(item.keys()) | length) == 0 + quiet: true + loop: "{{ multiple_containers.containers | sort(attribute='name') }}" + loop_control: + label: "{{ item.name }}" + + - name: Fetch all containers + openstack.cloud.object_containers_info: + cloud: "{{ cloud }}" + register: all_containers + + - name: Check that all expected containers were returned + ansible.builtin.assert: + that: + - item in (all_containers.containers | map(attribute='name')) + fail_msg: "Container not found: {{ item }}" + quiet: true + loop: "{{ all_test_containers }}" + + - name: Check that the expected fields are returned for all containers + ansible.builtin.assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - (expected_fields_multiple | difference(item.keys()) | length) == 0 + quiet: true + loop: "{{ all_containers.containers | selectattr('name', 'in', all_test_containers) }}" + loop_control: + label: "{{ item.name }}" + + always: + + - name: Delete all containers + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: "{{ item }}" + state: absent + delete_with_all_objects: true + loop: "{{ all_test_containers }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index dd7ddd99..4ff6ad8a 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -35,6 +35,7 @@ - { role: neutron_rbac_policy, tags: neutron_rbac_policy } - { role: object, tags: object } - { role: object_container, tags: object_container } + - { role: object_containers_info, tags: object_containers_info } - { role: port, tags: port } - { role: trait, tags: trait } - { role: trunk, tags: trunk } diff --git a/meta/runtime.yml b/meta/runtime.yml index 2ea6ae11..e7480b78 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -56,6 +56,7 @@ action_groups: - neutron_rbac_policy - object - object_container + - object_containers_info - port - port_info - project diff --git a/plugins/modules/object_containers_info.py b/plugins/modules/object_containers_info.py new file mode 100644 index 00000000..f40f3e9b --- /dev/null +++ b/plugins/modules/object_containers_info.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Catalyst Cloud Limited +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: object_containers_info +short_description: Fetch container info from the OpenStack Swift service. +author: OpenStack Ansible SIG +description: + - Fetch container info from the OpenStack Swift service. +options: + name: + description: + - Name of the container + type: str + aliases: ["container"] + prefix: + description: + - Filter containers by prefix + type: str +extends_documentation_fragment: + - openstack.cloud.openstack +""" + +EXAMPLES = r""" +- name: List all containers existing on the project + openstack.cloud.object_containers_info: + +- name: Retrive a single container by name + openstack.cloud.object_containers_info: + name: test-container + +- name: Retrieve and filter containers by prefix + openstack.cloud.object_containers_info: + prefix: test- +""" + +RETURN = r""" +containers: + description: List of dictionaries describing matching containers. + returned: always + type: list + elements: dict + contains: + bytes: + description: The total number of bytes that are stored in Object Storage + for the container. + type: int + sample: 5449 + bytes_used: + description: The count of bytes used in total. + type: int + sample: 5449 + content_type: + description: The MIME type of the list of names. + Only fetched when searching for a container by name. + type: str + sample: null + count: + description: The number of objects in the container. + type: int + sample: 1 + history_location: + description: Enables versioning on the container. + Only fetched when searching for a container by name. + type: str + sample: null + id: + description: The ID of the container. Equals I(name). + type: str + sample: "otc" + if_none_match: + description: "In combination with C(Expect: 100-Continue), specify an + C(If-None-Match: *) header to query whether the server + already has a copy of the object before any data is sent. + Only set when searching for a container by name." + type: str + sample: null + is_content_type_detected: + description: If set to C(true), Object Storage guesses the content type + based on the file extension and ignores the value sent in + the Content-Type header, if present. + Only fetched when searching for a container by name. + type: bool + sample: null + is_newest: + description: If set to True, Object Storage queries all replicas to + return the most recent one. If you omit this header, Object + Storage responds faster after it finds one valid replica. + Because setting this header to True is more expensive for + the back end, use it only when it is absolutely needed. + Only fetched when searching for a container by name. + type: bool + sample: null + meta_temp_url_key: + description: The secret key value for temporary URLs. If not set, + this header is not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null + meta_temp_url_key_2: + description: A second secret key value for temporary URLs. If not set, + this header is not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null + name: + description: The name of the container. + type: str + sample: "otc" + object_count: + description: The number of objects. + type: int + sample: 1 + read_ACL: + description: The ACL that grants read access. If not set, this header is + not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null + storage_policy: + description: Storage policy used by the container. It is not possible to + change policy of an existing container. + Only fetched when searching for a container by name. + type: str + sample: null + sync_key: + description: The secret key for container synchronization. If not set, + this header is not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null + sync_to: + description: The destination for container synchronization. If not set, + this header is not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null + timestamp: + description: The timestamp of the transaction. + Only fetched when searching for a container by name. + type: str + sample: null + versions_location: + description: Enables versioning on this container. The value is the name + of another container. You must UTF-8-encode and then + URL-encode the name before you include it in the header. To + disable versioning, set the header to an empty string. + Only fetched when searching for a container by name. + type: str + sample: null + write_ACL: + description: The ACL that grants write access. If not set, this header is + not returned by this operation. + Only fetched when searching for a container by name. + type: str + sample: null +""" + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ObjectContainersInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(aliases=["container"]), + prefix=dict(), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def run(self): + if self.params["name"]: + containers = [ + ( + self.conn.object_store.get_container_metadata( + self.params["name"], + ).to_dict(computed=False) + ), + ] + else: + query = {} + if self.params["prefix"]: + query["prefix"] = self.params["prefix"] + containers = [ + c.to_dict(computed=False) + for c in self.conn.object_store.containers(**query) + ] + self.exit(changed=False, containers=containers) + + +def main(): + module = ObjectContainersInfoModule() + module() + + +if __name__ == "__main__": + main()