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:
parent
1ca6f208f7
commit
7c536e69b3
32
ci/roles/object/defaults/main.yml
Normal file
32
ci/roles/object/defaults/main.yml
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -5,115 +5,341 @@
|
||||
# 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)
|
||||
|
||||
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
|
||||
<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:
|
||||
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()
|
||||
|
||||
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user