Refactored object{,_container} modules breaking backward compatibility

Previously both modules object and object_container had huge overlaps in
functionality. Both allowed to create and delete containers. One would
not have to pass a object to the object module at all and could use it
to manage containers only. Now the object module has been changed to
manage an object in a container only while the object_container module
is responsible for managing Swift containers only.

With object module it is now also possible to pass data instead of a
filename via module options. The container_access functionality has been
dropped from object module. It has been moved and extended as read_ACL
and write_ACL options in object_container module.

With object_container module it is now also possible to manage the
container access with read_ACL and write_ACL options. Those mirror
earlier container_access option of the object module which has been
removed.

Change-Id: I96fb9b946444866b157655e148250f1eda35e942
This commit is contained in:
Jakob Meng 2022-11-23 13:49:42 +01:00
parent 1ca6f208f7
commit 7c536e69b3
7 changed files with 768 additions and 304 deletions

View File

@ -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

View File

@ -1,37 +1,35 @@
--- ---
- name: Create a test object file
shell: mktemp
register: tmp_file
- name: Create container - name: Create container
openstack.cloud.object: openstack.cloud.object_container:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
container: ansible_container name: ansible_container
container_access: private
- name: Put object - name: Create object
openstack.cloud.object: openstack.cloud.object:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
name: ansible_object name: ansible_object
filename: "{{ tmp_file.stdout }}" data: "this is a test"
container: ansible_container 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 - name: Delete object
openstack.cloud.object: openstack.cloud.object:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: absent state: absent
name: ansible_object name: ansible_object
container: ansible_container container: ansible_container
- name: Delete container - name: Delete container
openstack.cloud.object: openstack.cloud.object_container:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: absent state: absent
container: ansible_container name: ansible_container
- name: Delete test object file
file:
name: "{{ tmp_file.stdout }}"
state: absent

View File

@ -1 +1,22 @@
container_name: "test-container" 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

View File

@ -1,63 +1,93 @@
--- ---
- module_defaults: - name: Create an empty container with public access
group/openstack.cloud.openstack: openstack.cloud.object_container:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
# Backward compatibility with Ansible 2.9 name: ansible_container
openstack.cloud.object_container: read_ACL: ".r:*,.rlistings"
cloud: "{{ cloud }}" register: container
block:
- name: Create an empty container
openstack.cloud.object_container:
container: "{{ container_name }}"
register: container
- name: Verify container was created - name: Assert return values of container module
assert: assert:
that: that:
- container is success - container is changed
- container is changed - container.container.name == "ansible_container"
- container.container.name == container_name - 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 - name: Set container metadata aka container properties
openstack.cloud.object_container: openstack.cloud.object_container:
container: "{{ container_name }}" cloud: "{{ cloud }}"
metadata: "Cache-Control='no-cache'" name: ansible_container
register: set_meta metadata:
'Cache-Control': 'no-cache'
'foo': 'bar'
register: container
- name: Verify container metadata was set - name: Verify container metadata was set
assert: assert:
that: that:
- set_meta is success - container is changed
- set_meta is changed - ('cache-control' in container.container.metadata.keys()|map('lower'))
- container.container.metadata['foo'] == 'bar'
- name: Delete some keys from container metadata - name: Update a container
openstack.cloud.object_container: openstack.cloud.object_container:
container: "{{ container_name }}" cloud: "{{ cloud }}"
keys: name: ansible_container
- Cache-Control delete_metadata_keys:
register: delete_meta - 'Cache-Control'
read_ACL: ""
register: container
- name: Verify some keys from container metadata was deleted - name: Verify updated container
assert: assert:
that: that:
- delete_meta is success - container is changed
- delete_meta 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 - name: Delete container
openstack.cloud.object_container: openstack.cloud.object_container:
container: "{{ container_name }}" cloud: "{{ cloud }}"
state: absent name: ansible_container
register: deleted state: absent
register: container
- name: Verify container was deleted - name: Verify container was deleted
assert: assert:
that: that:
- deleted is success - container is changed
- deleted is changed
always: - name: Delete container again
- name: Delete container openstack.cloud.object_container:
openstack.cloud.object_container: cloud: "{{ cloud }}"
container: "{{ container_name }}" name: ansible_container
state: absent state: absent
ignore_errors: yes 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

View File

@ -11,9 +11,6 @@
- { role: compute_flavor_access, tags: compute_flavor_access } - { role: compute_flavor_access, tags: compute_flavor_access }
- { role: config, tags: config } - { role: config, tags: config }
- { role: dns_zone_info, tags: dns_zone_info } - { role: dns_zone_info, tags: dns_zone_info }
- role: object_container
tags: object_container
when: sdk_version is version(0.18, '>=')
- role: dns - role: dns
tags: dns tags: dns
when: sdk_version is version(0.28, '>=') when: sdk_version is version(0.28, '>=')
@ -45,6 +42,7 @@
tags: nova_services tags: nova_services
when: sdk_version is version(0.44, '>=') when: sdk_version is version(0.44, '>=')
- { role: object, tags: object } - { role: object, tags: object }
- { role: object_container, tags: object_container }
- { role: port, tags: port } - { role: port, tags: port }
- { role: project, tags: project } - { role: project, tags: project }
- { role: project_info, tags: project_info } - { role: project_info, tags: project_info }

View File

@ -5,115 +5,341 @@
# Copyright (c) 2013, Benno Joy <benno@ansible.com> # Copyright (c) 2013, Benno Joy <benno@ansible.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = ''' DOCUMENTATION = r'''
--- ---
module: object 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 author: OpenStack Ansible SIG
description: description:
- Create or Delete objects and containers from OpenStack - Create or delete Swift objects in OpenStack clouds
options: options:
container: container:
description: description:
- The name of the container in which to create the object - The name (and ID) of the container in which to create the object in.
required: true - This container will not be created if it does not exist already.
type: str required: true
name: type: str
description: data:
- Name to be give to the object. If omitted, operations will be on description:
the entire container - The content to upload to the object.
required: false - Mutually exclusive with I(filename).
type: str - This attribute cannot be updated.
filename: type: str
description: filename:
- Path to local file to be uploaded. description:
required: false - The path to the local file whose contents will be uploaded.
type: str - Mutually exclusive with I(data).
container_access: type: str
description: name:
- desired container access level. description:
required: false - Name (and ID) of the object.
choices: ['private', 'public'] required: true
default: private type: str
type: str state:
state: description:
description: - Whether the object should be C(present) or C(absent).
- Should the resource be present or absent. choices: ['present', 'absent']
choices: [present, absent] default: present
default: present type: str
type: str
requirements: requirements:
- "python >= 3.6" - "python >= 3.6"
- "openstacksdk" - "openstacksdk"
extends_documentation_fragment: extends_documentation_fragment:
- openstack.cloud.openstack - openstack.cloud.openstack
''' '''
EXAMPLES = ''' RETURN = r'''
- name: "Create a object named 'fstab' in the 'config' container" 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
<container>/<object>.
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: openstack.cloud.object:
cloud: mordred cloud: mordred
state: present
name: fstab
container: config container: config
filename: /etc/fstab filename: /etc/fstab
name: fstab
state: present
- name: Delete a container called config and all of its contents - name: Delete a container called config and all of its contents
openstack.cloud.object: openstack.cloud.object:
cloud: rax-iad cloud: rax-iad
state: absent
container: config container: config
state: absent
''' '''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class SwiftObjectModule(OpenStackModule): class ObjectModule(OpenStackModule):
argument_spec = dict( argument_spec = dict(
name=dict(),
container=dict(required=True), container=dict(required=True),
data=dict(),
filename=dict(), filename=dict(),
container_access=dict(default='private', choices=['private', 'public']), name=dict(required=True),
state=dict(default='present', choices=['absent', 'present']), state=dict(default='present', choices=['absent', 'present']),
) )
module_kwargs = dict()
def process_object( module_kwargs = dict(
self, container, name, filename, container_access, **kwargs mutually_exclusive=[
): ('data', 'filename'),
changed = False ],
container_obj = self.conn.get_container(container) required_if=[
if kwargs['state'] == 'present': ('state', 'present', ('data', 'filename'), True),
if not container_obj: ],
container_obj = self.conn.create_container(container) supports_check_mode=True
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
def run(self): 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(): def main():
module = SwiftObjectModule() module = ObjectModule()
module() module()

View File

@ -4,106 +4,205 @@
# Copyright (c) 2021 by Open Telekom Cloud, operated by T-Systems International GmbH # 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) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = ''' DOCUMENTATION = r'''
--- ---
module: object_container module: object_container
short_description: Manage Swift container. short_description: Manage a Swift container.
author: OpenStack Ansible SIG author: OpenStack Ansible SIG
description: description:
- Manage Swift container. - Create, update and delete a Swift container.
options: 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: delete_with_all_objects:
description: description:
- Whether the container should be deleted with all objects or not. - Whether the container should be deleted recursively,
- Without this parameter set to "true", an attempt to delete a container that contains objects will fail. 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 type: bool
default: False 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: state:
description: Whether resource should be present or absent. description:
- Whether the object should be C(present) or C(absent).
default: 'present' default: 'present'
choices: ['present', 'absent'] choices: ['present', 'absent']
type: str type: str
requirements: requirements:
- "python >= 3.6" - "python >= 3.6"
- "openstacksdk" - "openstacksdk"
extends_documentation_fragment: extends_documentation_fragment:
- openstack.cloud.openstack - openstack.cloud.openstack
''' '''
RETURN = ''' RETURN = r'''
container: container:
description: Specifies the container. description: Dictionary describing the Swift container.
returned: On success when C(state=present) returned: On success when I(state) is C(present).
type: dict type: dict
sample: contains:
{ bytes:
"bytes": 5449, description: The total number of bytes that are stored in Object Storage
"bytes_used": 5449, for the container.
"content_type": null, type: int
"count": 1, sample: 5449
"id": "otc", bytes_used:
"if_none_match": null, description: The count of bytes used in total.
"is_content_type_detected": null, type: int
"is_newest": null, sample: 5449
"meta_temp_url_key": null, content_type:
"meta_temp_url_key_2": null, description: The MIME type of the list of names.
"name": "otc", type: str
"object_count": 1, sample: null
"read_ACL": null, count:
"sync_key": null, description: The number of objects in the container.
"sync_to": null, type: int
"timestamp": null, sample: 1
"versions_location": null, history_location:
"write_ACL": null 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 = ''' EXAMPLES = r'''
# Create empty container - name: Create empty container with public access
- openstack.cloud.object_container: openstack.cloud.object_container:
container: "new-container" name: "new-container"
state: present state: present
read_ACL: ".r:*,.rlistings"
# Set metadata for container - name: Set metadata for container
- openstack.cloud.object_container: openstack.cloud.object_container:
container: "new-container" name: "new-container"
metadata: "Cache-Control='no-cache'" metadata:
'Cache-Control': 'no-cache'
'foo': 'bar'
# Delete some keys from metadata of a container - name: Delete metadata keys of a container
- openstack.cloud.object_container: openstack.cloud.object_container:
container: "new-container" name: "new-container"
keys: delete_metadata_keys:
- content_type - foo
# Delete container - name: Delete container
- openstack.cloud.object_container: openstack.cloud.object_container:
container: "new-container" name: "new-container"
state: absent state: absent
# Delete container and its objects - name: Delete container and all its objects
- openstack.cloud.object_container: openstack.cloud.object_container:
container: "new-container" name: "new-container"
delete_with_all_objects: true delete_with_all_objects: true
state: absent state: absent
''' '''
@ -114,88 +213,148 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O
class ContainerModule(OpenStackModule): class ContainerModule(OpenStackModule):
argument_spec = dict( 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'), 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']), state=dict(default='present', choices=['present', 'absent']),
delete_with_all_objects=dict(type='bool', default=False) write_ACL=dict(),
) )
def create(self, container): module_kwargs = dict(
supports_check_mode=True
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
def run(self): def run(self):
container = self.params['container']
state = self.params['state'] 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'] 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': delete_metadata_keys = self.params['delete_metadata_keys']
self.delete(container) 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: if metadata:
self.set_metadata(container, metadata) container = self.conn.object_store.set_container_metadata(
if keys: container.name, refresh=True, **metadata)
self.delete_metadata(container, keys)
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(): def main():