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()