diff --git a/ci/roles/object/defaults/main.yml b/ci/roles/object/defaults/main.yml new file mode 100644 index 00000000..7c057ede --- /dev/null +++ b/ci/roles/object/defaults/main.yml @@ -0,0 +1,32 @@ +expected_fields: + - accept_ranges + - access_control_allow_origin + - content_disposition + - content_encoding + - content_length + - content_type + - copy_from + - delete_after + - delete_at + - etag + - expires_at + - id + - if_match + - if_modified_since + - if_none_match + - if_unmodified_since + - is_content_type_detected + - is_newest + - is_static_large_object + - last_modified_at + - manifest + - metadata + - multipart_manifest + - name + - object_manifest + - range + - signature + - symlink_target + - symlink_target_account + - timestamp + - transfer_encoding diff --git a/ci/roles/object/tasks/main.yml b/ci/roles/object/tasks/main.yml index a275cacf..be459157 100644 --- a/ci/roles/object/tasks/main.yml +++ b/ci/roles/object/tasks/main.yml @@ -1,37 +1,35 @@ --- -- name: Create a test object file - shell: mktemp - register: tmp_file - - name: Create container - openstack.cloud.object: - cloud: "{{ cloud }}" - state: present - container: ansible_container - container_access: private + openstack.cloud.object_container: + cloud: "{{ cloud }}" + state: present + name: ansible_container -- name: Put object +- name: Create object openstack.cloud.object: - cloud: "{{ cloud }}" - state: present - name: ansible_object - filename: "{{ tmp_file.stdout }}" - container: ansible_container + cloud: "{{ cloud }}" + state: present + name: ansible_object + data: "this is a test" + container: ansible_container + register: object + +- name: Assert return values of object module + assert: + that: + - object.object.id == "ansible_object" + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(object.object.keys())|length == 0 - name: Delete object openstack.cloud.object: - cloud: "{{ cloud }}" - state: absent - name: ansible_object - container: ansible_container + cloud: "{{ cloud }}" + state: absent + name: ansible_object + container: ansible_container - name: Delete container - openstack.cloud.object: - cloud: "{{ cloud }}" - state: absent - container: ansible_container - -- name: Delete test object file - file: - name: "{{ tmp_file.stdout }}" - state: absent + openstack.cloud.object_container: + cloud: "{{ cloud }}" + state: absent + name: ansible_container diff --git a/ci/roles/object_container/defaults/main.yml b/ci/roles/object_container/defaults/main.yml index b9bbed09..4bc6fd1f 100644 --- a/ci/roles/object_container/defaults/main.yml +++ b/ci/roles/object_container/defaults/main.yml @@ -1 +1,22 @@ -container_name: "test-container" \ No newline at end of file +expected_fields: + - 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 + - metadata + - name + - object_count + - read_ACL + - storage_policy + - sync_key + - sync_to + - timestamp + - versions_location + - write_ACL diff --git a/ci/roles/object_container/tasks/main.yml b/ci/roles/object_container/tasks/main.yml index 9787463a..ce4cbb5d 100644 --- a/ci/roles/object_container/tasks/main.yml +++ b/ci/roles/object_container/tasks/main.yml @@ -1,63 +1,93 @@ --- -- module_defaults: - group/openstack.cloud.openstack: - cloud: "{{ cloud }}" - # Backward compatibility with Ansible 2.9 - openstack.cloud.object_container: - cloud: "{{ cloud }}" - block: - - name: Create an empty container - openstack.cloud.object_container: - container: "{{ container_name }}" - register: container +- name: Create an empty container with public access + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container + read_ACL: ".r:*,.rlistings" + register: container - - name: Verify container was created - assert: - that: - - container is success - - container is changed - - container.container.name == container_name +- name: Assert return values of container module + assert: + that: + - container is changed + - container.container.name == "ansible_container" + - container.container.read_ACL == ".r:*,.rlistings" + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(container.container.keys())|length == 0 - - name: Set metadata for a container - openstack.cloud.object_container: - container: "{{ container_name }}" - metadata: "Cache-Control='no-cache'" - register: set_meta +- name: Set container metadata aka container properties + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container + metadata: + 'Cache-Control': 'no-cache' + 'foo': 'bar' + register: container - - name: Verify container metadata was set - assert: - that: - - set_meta is success - - set_meta is changed +- name: Verify container metadata was set + assert: + that: + - container is changed + - ('cache-control' in container.container.metadata.keys()|map('lower')) + - container.container.metadata['foo'] == 'bar' - - name: Delete some keys from container metadata - openstack.cloud.object_container: - container: "{{ container_name }}" - keys: - - Cache-Control - register: delete_meta +- name: Update a container + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container + delete_metadata_keys: + - 'Cache-Control' + read_ACL: "" + register: container - - name: Verify some keys from container metadata was deleted - assert: - that: - - delete_meta is success - - delete_meta is changed +- name: Verify updated container + assert: + that: + - container is changed + - ('cache-control' not in container.container.metadata.keys()|map('lower')) + - "container.container.metadata == {'foo': 'bar'}" + - container.container.read_ACL is none or container.container.read_ACL == "" - - name: Delete container - openstack.cloud.object_container: - container: "{{ container_name }}" - state: absent - register: deleted +- name: Delete container + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container + state: absent + register: container - - name: Verify container was deleted - assert: - that: - - deleted is success - - deleted is changed +- name: Verify container was deleted + assert: + that: + - container is changed - always: - - name: Delete container - openstack.cloud.object_container: - container: "{{ container_name }}" - state: absent - ignore_errors: yes +- name: Delete container again + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container + state: absent + register: container + +- name: Verify container was not deleted again + assert: + that: + - container is not changed + +- name: Create another container for recursive deletion + openstack.cloud.object_container: + cloud: "{{ cloud }}" + name: ansible_container2 + +- name: Load an object into container + openstack.cloud.object: + cloud: "{{ cloud }}" + state: present + name: ansible_object + data: "this is another test" + container: ansible_container2 + +- name: Delete container recursively + openstack.cloud.object_container: + cloud: "{{ cloud }}" + state: absent + name: ansible_container2 + delete_with_all_objects: yes diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 9bc11383..7f9c456d 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -11,9 +11,6 @@ - { role: compute_flavor_access, tags: compute_flavor_access } - { role: config, tags: config } - { role: dns_zone_info, tags: dns_zone_info } - - role: object_container - tags: object_container - when: sdk_version is version(0.18, '>=') - role: dns tags: dns when: sdk_version is version(0.28, '>=') @@ -45,6 +42,7 @@ tags: nova_services when: sdk_version is version(0.44, '>=') - { role: object, tags: object } + - { role: object_container, tags: object_container } - { role: port, tags: port } - { role: project, tags: project } - { role: project_info, tags: project_info } diff --git a/plugins/modules/object.py b/plugins/modules/object.py index 38a40f45..d28546bc 100644 --- a/plugins/modules/object.py +++ b/plugins/modules/object.py @@ -5,115 +5,341 @@ # Copyright (c) 2013, Benno Joy # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: object -short_description: Create or Delete objects and containers from OpenStack +short_description: Create or delete Swift objects in OpenStack clouds author: OpenStack Ansible SIG description: - - Create or Delete objects and containers from OpenStack + - Create or delete Swift objects in OpenStack clouds options: - container: - description: - - The name of the container in which to create the object - required: true - type: str - name: - description: - - Name to be give to the object. If omitted, operations will be on - the entire container - required: false - type: str - filename: - description: - - Path to local file to be uploaded. - required: false - type: str - container_access: - description: - - desired container access level. - required: false - choices: ['private', 'public'] - default: private - type: str - state: - description: - - Should the resource be present or absent. - choices: [present, absent] - default: present - type: str + container: + description: + - The name (and ID) of the container in which to create the object in. + - This container will not be created if it does not exist already. + required: true + type: str + data: + description: + - The content to upload to the object. + - Mutually exclusive with I(filename). + - This attribute cannot be updated. + type: str + filename: + description: + - The path to the local file whose contents will be uploaded. + - Mutually exclusive with I(data). + type: str + name: + description: + - Name (and ID) of the object. + required: true + type: str + state: + description: + - Whether the object should be C(present) or C(absent). + choices: ['present', 'absent'] + default: present + type: str requirements: - - "python >= 3.6" - - "openstacksdk" - + - "python >= 3.6" + - "openstacksdk" extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -EXAMPLES = ''' -- name: "Create a object named 'fstab' in the 'config' container" +RETURN = r''' +object: + description: Dictionary describing the object. + returned: On success when I(state) is C(present). + type: dict + contains: + accept_ranges: + description: The type of ranges that the object accepts. + type: str + access_control_allow_origin: + description: CORS for RAX (deviating from standard) + type: str + content_disposition: + description: If set, specifies the override behavior for the browser. + For example, this header might specify that the browser use + a download program to save this file rather than show the + file, which is the default. If not set, this header is not + returned by this operation. + type: str + content_encoding: + description: If set, the value of the Content-Encoding metadata. + If not set, this header is not returned by this operation. + type: str + content_length: + description: HEAD operations do not return content. However, in this + operation the value in the Content-Length header is not the + size of the response body. Instead it contains the size of + the object, in bytes. + type: str + content_type: + description: The MIME type of the object. + type: int + copy_from: + description: If set, this is the name of an object used to create the new + object by copying the X-Copy-From object. The value is in + form {container}/{object}. You must UTF-8-encode and then + URL-encode the names of the container and object before you + include them in the header. Using PUT with X-Copy-From has + the same effect as using the COPY operation to copy an + object. + type: str + delete_after: + description: Specifies the number of seconds after which the object is + removed. Internally, the Object Storage system stores this + value in the X-Delete-At metadata item. + type: int + delete_at: + description: If set, the time when the object will be deleted by the + system in the format of a UNIX Epoch timestamp. If not set, + this header is not returned by this operation. + type: str + etag: + description: For objects smaller than 5 GB, this value is the MD5 + checksum of the object content. The value is not quoted. + For manifest objects, this value is the MD5 checksum of the + concatenated string of MD5 checksums and ETags for each of + the segments in the manifest, and not the MD5 checksum of + the content that was downloaded. Also the value is enclosed + in double-quote characters. + You are strongly recommended to compute the MD5 checksum of + the response body as it is received and compare this value + with the one in the ETag header. If they differ, the content + was corrupted, so retry the operation. + type: str + expires_at: + description: Used with temporary URLs to specify the expiry time of the + signature. For more information about temporary URLs, see + OpenStack Object Storage API v1 Reference. + type: str + id: + description: ID of the object. Equal to C(name). + type: str + if_match: + description: See U(http://www.ietf.org/rfc/rfc2616.txt). + type: list + if_modified_since: + description: See U(http://www.ietf.org/rfc/rfc2616.txt). + type: str + 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." + type: list + if_unmodified_since: + description: See U(http://www.ietf.org/rfc/rfc2616.txt). + type: str + is_content_type_detected: + description: If set to true, Object Storage guesses the content type + based on the file extension and ignores the value sent in + the Content-Type header, if present. + type: bool + 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. + type: bool + is_static_large_object: + description: Set to True if this object is a static large object manifest + object. + type: bool + last_modified_at: + description: The date and time that the object was created or the last + time that the metadata was changed. + type: str + manifest: + description: If present, this is a dynamic large object manifest object. + The value is the container and object name prefix of the + segment objects in the form container/prefix. + type: str + multipart_manifest: + description: If you include the multipart-manifest=get query parameter + and the object is a large object, the object contents are + not returned. Instead, the manifest is returned in the + X-Object-Manifest response header for dynamic large objects + or in the response body for static large objects. + type: str + name: + description: Name of the object. + returned: success + type: str + object_manifest: + description: If set, to this is a dynamic large object manifest object. + The value is the container and object name prefix of the + segment objects in the form container/prefix. + type: str + range: + description: TODO. + type: dict + signature: + description: Used with temporary URLs to sign the request. For more + information about temporary URLs, see OpenStack Object + Storage API v1 Reference. + type: str + symlink_target: + description: If present, this is a symlink object. The value is the + relative path of the target object in the format + /. + type: str + symlink_target_account: + description: If present, and X-Symlink-Target is present, then this is a + cross-account symlink to an object in the account specified + in the value. + type: str + timestamp: + description: The timestamp of the transaction. + type: str + transfer_encoding: + description: Set to chunked to enable chunked transfer encoding. If used, + do not set the Content-Length header to a non-zero value. + type: str +''' + +EXAMPLES = r''' +- name: Create a object named 'fstab' in the 'config' container openstack.cloud.object: cloud: mordred - state: present - name: fstab container: config filename: /etc/fstab + name: fstab + state: present - name: Delete a container called config and all of its contents openstack.cloud.object: cloud: rax-iad - state: absent container: config + state: absent ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -class SwiftObjectModule(OpenStackModule): +class ObjectModule(OpenStackModule): argument_spec = dict( - name=dict(), container=dict(required=True), + data=dict(), filename=dict(), - container_access=dict(default='private', choices=['private', 'public']), + name=dict(required=True), state=dict(default='present', choices=['absent', 'present']), ) - module_kwargs = dict() - def process_object( - self, container, name, filename, container_access, **kwargs - ): - changed = False - container_obj = self.conn.get_container(container) - if kwargs['state'] == 'present': - if not container_obj: - container_obj = self.conn.create_container(container) - changed = True - if self.conn.get_container_access(container) != container_access: - self.conn.set_container_access(container, container_access) - changed = True - if name: - if self.conn.is_object_stale(container, name, filename): - self.conn.create_object(container, name, filename) - changed = True - else: - if container_obj: - if name: - if self.conn.get_object_metadata(container, name): - self.conn.delete_object(container, name) - changed = True - else: - self.conn.delete_container(container) - changed = True - return changed + module_kwargs = dict( + mutually_exclusive=[ + ('data', 'filename'), + ], + required_if=[ + ('state', 'present', ('data', 'filename'), True), + ], + supports_check_mode=True + ) def run(self): - changed = self.process_object(**self.params) + state = self.params['state'] + object = self._find() - self.exit_json(changed=changed) + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, object)) + + if state == 'present' and not object: + # Create object + object = self._create() + self.exit_json(changed=True, + # metadata is not returned by + # to_dict(computed=False) so return it explicitly + object=dict(metadata=object.metadata, + **object.to_dict(computed=False))) + + elif state == 'present' and object: + # Update object + update = self._build_update(object) + if update: + object = self._update(object, update) + + self.exit_json(changed=bool(update), + # metadata is not returned by + # to_dict(computed=False) so return it explicitly + object=dict(metadata=object.metadata, + **object.to_dict(computed=False))) + + elif state == 'absent' and object: + # Delete object + self._delete(object) + self.exit_json(changed=True) + + elif state == 'absent' and not object: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, object): + update = {} + + container_name = self.params['container'] + + filename = self.params['filename'] + if filename is not None: + if self.conn.object_store.is_object_stale(container_name, + object.id, filename): + update['filename'] = filename + + return update + + def _create(self): + name = self.params['name'] + container_name = self.params['container'] + + kwargs = dict((k, self.params[k]) + for k in ['data', 'filename'] + if self.params[k] is not None) + + return self.conn.object_store.create_object(container_name, name, + **kwargs) + + def _delete(self, object): + container_name = self.params['container'] + self.conn.object_store.delete_object(object.id, + container=container_name) + + def _find(self): + name_or_id = self.params['name'] + container_name = self.params['container'] + # openstacksdk has no object_store.find_object() function + try: + return self.conn.object_store.get_object(name_or_id, + container=container_name) + except self.sdk.exceptions.ResourceNotFound: + return None + + def _update(self, object, update): + filename = update.get('filename') + if filename: + container_name = self.params['container'] + object = self.conn.object_store.create_object(container_name, + object.id, + filename=filename) + + return object + + def _will_change(self, state, object): + if state == 'present' and not object: + return True + elif state == 'present' and object: + return bool(self._build_update(object)) + elif state == 'absent' and object: + return True + else: + # state == 'absent' and not object: + return False def main(): - module = SwiftObjectModule() + module = ObjectModule() module() diff --git a/plugins/modules/object_container.py b/plugins/modules/object_container.py index 5beef82b..984ed6ca 100644 --- a/plugins/modules/object_container.py +++ b/plugins/modules/object_container.py @@ -4,106 +4,205 @@ # Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: object_container -short_description: Manage Swift container. +short_description: Manage a Swift container. author: OpenStack Ansible SIG description: - - Manage Swift container. + - Create, update and delete a Swift container. options: - container: - description: Name of a container in Swift. - type: str - required: true - metadata: - description: - - Key/value pairs to be set as metadata on the container. - - If a container doesn't exist, it will be created. - - Both custom and system metadata can be set. - - Custom metadata are keys and values defined by the user. - - The system metadata keys are content_type, content_encoding, content_disposition, delete_after,\ - delete_at, is_content_type_detected - type: dict - required: false - keys: - description: Keys from 'metadata' to be deleted. - type: list - elements: str - required: false delete_with_all_objects: description: - - Whether the container should be deleted with all objects or not. - - Without this parameter set to "true", an attempt to delete a container that contains objects will fail. + - Whether the container should be deleted recursively, + i.e. including all of its objects. + - If I(delete_with_all_objects) is set to C(false), an attempt to + delete a container which contains objects will fail. type: bool default: False - required: false + delete_metadata_keys: + description: + - Keys from I(metadata) to be deleted. + - "I(metadata) has precedence over I(delete_metadata_keys): If any + key is present in both options, then it will be created or updated, + not deleted." + - Metadata keys are case-insensitive. + type: list + elements: str + aliases: ['keys'] + metadata: + description: + - Key value pairs to be set as metadata on the container. + - Both custom and system metadata can be set. + - Custom metadata are keys and values defined by the user. + - I(metadata) is the same as setting properties in openstackclient with + C(openstack container set --property ...). + - Metadata keys are case-insensitive. + type: dict + name: + description: + - Name (and ID) of a Swift container. + type: str + required: true + aliases: ['container'] + read_ACL: + description: + - The ACL that grants read access. + - For example, use C(.r:*,.rlistings) for public access + and C('') for private access. + type: str + write_ACL: + description: + - The ACL that grants write access. + type: str state: - description: Whether resource should be present or absent. + description: + - Whether the object should be C(present) or C(absent). default: 'present' choices: ['present', 'absent'] type: str requirements: - - "python >= 3.6" - - "openstacksdk" - + - "python >= 3.6" + - "openstacksdk" extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -RETURN = ''' +RETURN = r''' container: - description: Specifies the container. - returned: On success when C(state=present) + description: Dictionary describing the Swift container. + returned: On success when I(state) is C(present). type: dict - sample: - { - "bytes": 5449, - "bytes_used": 5449, - "content_type": null, - "count": 1, - "id": "otc", - "if_none_match": null, - "is_content_type_detected": null, - "is_newest": null, - "meta_temp_url_key": null, - "meta_temp_url_key_2": null, - "name": "otc", - "object_count": 1, - "read_ACL": null, - "sync_key": null, - "sync_to": null, - "timestamp": null, - "versions_location": null, - "write_ACL": null - } + 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. + 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. + 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." + 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. + 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. + 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. + 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. + 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. + type: str + sample: null + storage_policy: + description: Storage policy used by the container. It is not possible to + change policy of an existing container. + type: str + sample: null + sync_key: + description: The secret key for container synchronization. If not set, + this header is not returned by this operation. + type: str + sample: null + sync_to: + description: The destination for container synchronization. If not set, + this header is not returned by this operation. + type: str + sample: null + timestamp: + description: The timestamp of the transaction. + 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. + type: str + sample: null + write_ACL: + description: The ACL that grants write access. If not set, this header is + not returned by this operation. + type: str + sample: null ''' -EXAMPLES = ''' -# Create empty container - - openstack.cloud.object_container: - container: "new-container" +EXAMPLES = r''' +- name: Create empty container with public access + openstack.cloud.object_container: + name: "new-container" state: present + read_ACL: ".r:*,.rlistings" -# Set metadata for container - - openstack.cloud.object_container: - container: "new-container" - metadata: "Cache-Control='no-cache'" +- name: Set metadata for container + openstack.cloud.object_container: + name: "new-container" + metadata: + 'Cache-Control': 'no-cache' + 'foo': 'bar' -# Delete some keys from metadata of a container - - openstack.cloud.object_container: - container: "new-container" - keys: - - content_type +- name: Delete metadata keys of a container + openstack.cloud.object_container: + name: "new-container" + delete_metadata_keys: + - foo -# Delete container - - openstack.cloud.object_container: - container: "new-container" +- name: Delete container + openstack.cloud.object_container: + name: "new-container" state: absent -# Delete container and its objects - - openstack.cloud.object_container: - container: "new-container" +- name: Delete container and all its objects + openstack.cloud.object_container: + name: "new-container" delete_with_all_objects: true state: absent ''' @@ -114,88 +213,148 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class ContainerModule(OpenStackModule): argument_spec = dict( - container=dict(required=True), + delete_metadata_keys=dict(type='list', elements='str', + no_log=False, # := noqa no-log-needed + aliases=['keys']), + delete_with_all_objects=dict(type='bool', default=False), metadata=dict(type='dict'), - keys=dict(type='list', elements='str', no_log=False), + name=dict(required=True, aliases=['container']), + read_ACL=dict(), state=dict(default='present', choices=['present', 'absent']), - delete_with_all_objects=dict(type='bool', default=False) + write_ACL=dict(), ) - def create(self, container): - - data = {} - if self._container_exist(container): - self.exit_json(changed=False) - - container_data = self.conn.object_store.create_container(name=container).to_dict() - container_data.pop('location') - data['container'] = container_data - self.exit_json(changed=True, **data) - - def delete(self, container): - - delete_with_all_objects = self.params['delete_with_all_objects'] - - changed = False - if self._container_exist(container): - objects = [] - for raw in self.conn.object_store.objects(container): - dt = raw.to_dict() - dt.pop('location') - objects.append(dt) - if len(objects) > 0: - if delete_with_all_objects: - for obj in objects: - self.conn.object_store.delete_object(container=container, obj=obj['id']) - else: - self.fail_json(msg="Container has objects") - self.conn.object_store.delete_container(container=container) - changed = True - - self.exit(changed=changed) - - def set_metadata(self, container, metadata): - - data = {} - - if not self._container_exist(container): - new_container = self.conn.object_store.create_container(name=container).to_dict() - - new_container = self.conn.object_store.set_container_metadata(container, **metadata).to_dict() - new_container.pop('location') - data['container'] = new_container - self.exit(changed=True, **data) - - def delete_metadata(self, container, keys): - - if not self._container_exist(container): - self.fail_json(msg="Container doesn't exist") - - self.conn.object_store.delete_container_metadata(container=container, keys=keys) - self.exit(changed=True) - - def _container_exist(self, container): - try: - self.conn.object_store.get_container_metadata(container) - return True - except self.sdk.exceptions.ResourceNotFound: - return False + module_kwargs = dict( + supports_check_mode=True + ) def run(self): - - container = self.params['container'] state = self.params['state'] + container = self._find() + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, container)) + + if state == 'present' and not container: + # Create container + container = self._create() + self.exit_json(changed=True, + # metadata is not returned by + # to_dict(computed=False) so return it explicitly + container=dict(metadata=container.metadata, + **container.to_dict(computed=False))) + + elif state == 'present' and container: + # Update container + update = self._build_update(container) + if update: + container = self._update(container, update) + + self.exit_json(changed=bool(update), + # metadata is not returned by + # to_dict(computed=False) so return it explicitly + container=dict(metadata=container.metadata, + **container.to_dict(computed=False))) + + elif state == 'absent' and container: + # Delete container + self._delete(container) + self.exit_json(changed=True) + + elif state == 'absent' and not container: + # Do nothing + self.exit_json(changed=False) + + def _build_update(self, container): + update = {} + metadata = self.params['metadata'] - keys = self.params['keys'] + if metadata is not None: + # Swift metadata keys must be treated as case-insensitive + old_metadata = dict((k.lower(), v) + for k, v in (container.metadata or {})) + new_metadata = dict((k, v) for k, v in metadata.items() + if k.lower() not in old_metadata + or v != old_metadata[k.lower()]) + if new_metadata: + update['metadata'] = new_metadata - if state == 'absent': - self.delete(container) + delete_metadata_keys = self.params['delete_metadata_keys'] + if delete_metadata_keys is not None: + for key in delete_metadata_keys: + if (container.metadata is not None + and key.lower() in [k.lower() + for k in container.metadata.keys()]): + update['delete_metadata_keys'] = delete_metadata_keys + break + + attributes = dict((k, self.params[k]) + for k in ['read_ACL', 'write_ACL'] + if self.params[k] is not None + and self.params[k] != container[k]) + + if attributes: + update['attributes'] = attributes + + return update + + def _create(self): + kwargs = dict((k, self.params[k]) + for k in ['metadata', 'name', 'read_ACL', 'write_ACL'] + if self.params[k] is not None) + + return self.conn.object_store.create_container(**kwargs) + + def _delete(self, container): + if self.params['delete_with_all_objects']: + for object in self.conn.object_store.objects(container.name): + self.conn.object_store.delete_object(obj=object.name, + container=container.name) + + self.conn.object_store.delete_container(container=container.name) + + def _find(self): + name_or_id = self.params['name'] + # openstacksdk has no container_store.find_container() function + try: + return self.conn.object_store.get_container_metadata(name_or_id) + except self.sdk.exceptions.ResourceNotFound: + return None + + def _update(self, container, update): + delete_metadata_keys = update.get('delete_metadata_keys') + if delete_metadata_keys: + self.conn.object_store.delete_container_metadata( + container=container.name, keys=delete_metadata_keys) + # object_store.delete_container_metadata() does not delete keys + # from metadata dictionary so reload container + container = \ + self.conn.object_store.get_container_metadata(container.name) + + # metadata has higher precedence than delete_metadata_keys + # and thus is updated after later + metadata = update.get('metadata') if metadata: - self.set_metadata(container, metadata) - if keys: - self.delete_metadata(container, keys) + container = self.conn.object_store.set_container_metadata( + container.name, refresh=True, **metadata) - self.create(container) + attributes = update.get('attributes') + if attributes: + container = self.conn.object_store.set_container_metadata( + container.name, refresh=True, **attributes) + + return container + + def _will_change(self, state, container): + if state == 'present' and not container: + return True + elif state == 'present' and container: + return bool(self._build_update(container)) + elif state == 'absent' and container: + return True + else: + # state == 'absent' and not container: + return False def main():