From 4ae0638048a572f2fbab71047d4d33e63dc6819a Mon Sep 17 00:00:00 2001 From: Everett Toews Date: Tue, 2 Feb 2016 17:19:06 -0600 Subject: [PATCH] Make metadata handling consistent in Object Store Make metadata handling consistent across all Object Store resources. The consistent methods are get_*_metdata, set_*_metadata, and delete_*_metadata which is similar to the __get__, __set__, and __delete__ methods of a descriptor or the __getitem__, __setitem__, and __delitem__ of a MutableMapping so it should be fairly natural for Python users. Change-Id: Ie6a892b48ac63ef6a6832b66f9e65e1b5d8a6d76 Closes-Bug: #1487150 --- openstack/object_store/v1/_base.py | 46 ++++ openstack/object_store/v1/_proxy.py | 201 +++++++++++------- openstack/object_store/v1/account.py | 2 + openstack/object_store/v1/container.py | 15 +- openstack/object_store/v1/obj.py | 83 +++++++- .../functional/compute/v2/test_server.py | 9 +- .../object_store/v1/test_account.py | 79 +++++++ .../object_store/v1/test_container.py | 108 +++++++++- .../functional/object_store/v1/test_obj.py | 117 +++++++++- 9 files changed, 555 insertions(+), 105 deletions(-) create mode 100644 openstack/tests/functional/object_store/v1/test_account.py diff --git a/openstack/object_store/v1/_base.py b/openstack/object_store/v1/_base.py index c870e01b2..17df93555 100644 --- a/openstack/object_store/v1/_base.py +++ b/openstack/object_store/v1/_base.py @@ -18,6 +18,52 @@ from openstack import resource class BaseResource(resource.Resource): service = object_store_service.ObjectStoreService() + #: Metadata stored for this resource. *Type: dict* + metadata = dict() + + _custom_metadata_prefix = None + _system_metadata = dict() + + def _calculate_headers(self, metadata): + headers = dict() + for key in metadata: + if key in self._system_metadata: + header = self._system_metadata[key] + else: + header = self._custom_metadata_prefix + key + headers[header] = metadata[key] + return headers + + def set_metadata(self, session, metadata): + url = self._get_url(self, self.id) + session.post(url, endpoint_filter=self.service, + headers=self._calculate_headers(metadata)) + + def delete_metadata(self, session, keys): + url = self._get_url(self, self.id) + headers = {key: '' for key in keys} + session.post(url, endpoint_filter=self.service, + headers=self._calculate_headers(headers)) + + def _set_metadata(self): + self.metadata = dict() + headers = self.get_headers() + + for header in headers: + if header.startswith(self._custom_metadata_prefix): + key = header[len(self._custom_metadata_prefix):].lower() + self.metadata[key] = headers[header] + + def get(self, session, include_headers=False, args=None): + super(BaseResource, self).get(session, include_headers, args) + self._set_metadata() + return self + + def head(self, session): + super(BaseResource, self).head(session) + self._set_metadata() + return self + @classmethod def update_by_id(cls, session, resource_id, attrs, path_args=None): """Update a Resource with the given attributes. diff --git a/openstack/object_store/v1/_proxy.py b/openstack/object_store/v1/_proxy.py index a576ec0ee..2040332d1 100644 --- a/openstack/object_store/v1/_proxy.py +++ b/openstack/object_store/v1/_proxy.py @@ -19,30 +19,36 @@ from openstack import proxy class Proxy(proxy.BaseProxy): def get_account_metadata(self): - """Get metatdata for this account + """Get metadata for this account. :rtype: :class:`~openstack.object_store.v1.account.Account` """ return self._head(_account.Account) - def set_account_metadata(self, account): - """Set metatdata for this account. + def set_account_metadata(self, **metadata): + """Set metadata for this account. - :param account: Account metadata specified on a - :class:`~openstack.object_store.v1.account.Account` object - to be sent to the server. - :type account: - :class:`~openstack.object_store.v1.account.Account` - - :rtype: ``None`` + :param kwargs metadata: Key/value pairs to be set as metadata + on the container. Custom metadata can be set. + Custom metadata are keys and values defined + by the user. """ - account.update(self.session) + account = self._get_resource(_account.Account, None) + account.set_metadata(self.session, metadata) + + def delete_account_metadata(self, keys): + """Delete metadata for this account. + + :param list keys: The keys of metadata to be deleted. + """ + account = self._get_resource(_account.Account, None) + account.delete_metadata(self.session, keys) def containers(self, **query): """Obtain Container objects for this account. - :param kwargs \*\*query: Optional query parameters to be sent to limit + :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :rtype: A generator of @@ -50,30 +56,6 @@ class Proxy(proxy.BaseProxy): """ return _container.Container.list(self.session, **query) - def get_container_metadata(self, container): - """Get metatdata for a container - - :param container: The value can be the name of a container or a - :class:`~openstack.object_store.v1.container.Container` - instance. - - :returns: One :class:`~openstack.object_store.v1.container.Container` - :raises: :class:`~openstack.exceptions.ResourceNotFound` - when no resource can be found. - """ - return self._head(_container.Container, container) - - def set_container_metadata(self, container): - """Set metatdata for a container. - - :param container: A container object containing metadata to be set. - :type container: - :class:`~openstack.object_store.v1.container.Container` - - :rtype: ``None`` - """ - container.create(self.session) - def create_container(self, **attrs): """Create a new container from attributes @@ -103,6 +85,54 @@ class Proxy(proxy.BaseProxy): self._delete(_container.Container, container, ignore_missing=ignore_missing) + def get_container_metadata(self, container): + """Get metadata for a container + + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + + :returns: One :class:`~openstack.object_store.v1.container.Container` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._head(_container.Container, container) + + def set_container_metadata(self, container, **metadata): + """Set metadata for a container. + + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + :param kwargs metadata: 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. System + metadata are keys defined by the Object Store + and values defined by the user. The system + metadata keys are: + - `content_type` + - `detect_content_type` + - `versions_location` + - `read_ACL` + - `write_ACL` + - `sync_to` + - `sync_key` + """ + res = self._get_resource(_container.Container, container) + res.set_metadata(self.session, metadata) + + def delete_container_metadata(self, container, keys): + """Delete metadata for a container. + + :param container: The value can be the ID of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + :param list keys: The keys of metadata to be deleted. + """ + res = self._get_resource(_container.Container, container) + res.delete_metadata(self.session, keys) + def objects(self, container, **query): """Return a generator that yields the Container's objects. @@ -121,28 +151,24 @@ class Proxy(proxy.BaseProxy): objs = _obj.Object.list(self.session, path_args={"container": container.name}, **query) - # TODO(briancurtin): Objects have to know their container at this - # point, otherwise further operations like getting their metadata - # or downloading them is a hassle because the end-user would have - # to maintain both the container and the object separately. - for ob in objs: - ob.container = container.name - yield ob + for obj in objs: + obj.container = container.name + yield obj - def _get_container_name(self, object, container): - if isinstance(object, _obj.Object): - if object.container is not None: - return object.container + def _get_container_name(self, obj, container): + if isinstance(obj, _obj.Object): + if obj.container is not None: + return obj.container if container is not None: container = _container.Container.from_id(container) return container.name raise ValueError("container must be specified") - def get_object(self, object, container=None): + def get_object(self, obj, container=None): """Get the data associated with an object - :param object: The value can be the name of an object or a + :param obj: The value can be the name of an object or a :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` @@ -154,15 +180,15 @@ class Proxy(proxy.BaseProxy): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - container_name = self._get_container_name(object, container) + container_name = self._get_container_name(obj, container) - return self._get(_obj.Object, object, + return self._get(_obj.Object, obj, path_args={"container": container_name}) - def download_object(self, object, container=None, path=None): + def download_object(self, obj, container=None, path=None): """Download the data contained inside an object to disk. - :param object: The value can be the name of an object or a + :param obj: The value can be the name of an object or a :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the name of a container or a :class:`~openstack.object_store.v1.container.Container` @@ -173,7 +199,7 @@ class Proxy(proxy.BaseProxy): when no resource can be found. """ with open(path, "w") as out: - out.write(self.get_object(object, container)) + out.write(self.get_object(obj, container)) def upload_object(self, **attrs): """Upload a new object from attributes @@ -199,10 +225,10 @@ class Proxy(proxy.BaseProxy): """Copy an object.""" raise NotImplementedError - def delete_object(self, object, ignore_missing=True, container=None): + def delete_object(self, obj, ignore_missing=True, container=None): """Delete an object - :param object: The value can be either the name of an object or a + :param obj: The value can be either the name of an object or a :class:`~openstack.object_store.v1.container.Container` instance. :param container: The value can be the ID of a container or a @@ -216,17 +242,16 @@ class Proxy(proxy.BaseProxy): :returns: ``None`` """ - container_name = self._get_container_name(object, container) + container_name = self._get_container_name(obj, container) - self._delete(_obj.Object, object, ignore_missing=ignore_missing, + self._delete(_obj.Object, obj, ignore_missing=ignore_missing, path_args={"container": container_name}) - def get_object_metadata(self, object, container=None): - """Get metatdata for an object + def get_object_metadata(self, obj, container=None): + """Get metadata for an object. - :param object: The value is an - :class:`~openstack.object_store.v1.obj.Object` - instance. + :param obj: The value can be the name of an object or a + :class:`~openstack.object_store.v1.obj.Object` instance. :param container: The value can be the ID of a container or a :class:`~openstack.object_store.v1.container.Container` instance. @@ -235,17 +260,51 @@ class Proxy(proxy.BaseProxy): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - container_name = self._get_container_name(object, container) + container_name = self._get_container_name(obj, container) - return self._head(_obj.Object, object, + return self._head(_obj.Object, obj, path_args={"container": container_name}) - def set_object_metadata(self, object): - """Set metatdata for an object. + def set_object_metadata(self, obj, container=None, **metadata): + """Set metadata for an object. - :param object: The object to set metadata for. - :type object: :class:`~openstack.object_store.v1.obj.Object` + Note: This method will do an extra HEAD call. - :rtype: ``None`` + :param obj: The value can be the name of an object or a + :class:`~openstack.object_store.v1.obj.Object` instance. + :param container: The value can be the name of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + :param kwargs metadata: 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. System + metadata are keys defined by the Object Store + and values defined by the user. The system + metadata keys are: + - `content_type` + - `content_encoding` + - `content_disposition` + - `detect_content_type` + - `delete_after` + - `delete_at` """ - object.create(self.session) + container_name = self._get_container_name(obj, container) + res = self._get_resource(_obj.Object, obj, + path_args={"container": container_name}) + res.set_metadata(self.session, metadata) + + def delete_object_metadata(self, obj, container=None, keys=None): + """Delete metadata for an object. + + :param obj: The value can be the name of an object or a + :class:`~openstack.object_store.v1.obj.Object` instance. + :param container: The value can be the ID of a container or a + :class:`~openstack.object_store.v1.container.Container` + instance. + :param list keys: The keys of metadata to be deleted. + """ + container_name = self._get_container_name(obj, container) + res = self._get_resource(_obj.Object, obj, + path_args={"container": container_name}) + res.delete_metadata(self.session, keys) diff --git a/openstack/object_store/v1/account.py b/openstack/object_store/v1/account.py index a8d53b844..aa59811bf 100644 --- a/openstack/object_store/v1/account.py +++ b/openstack/object_store/v1/account.py @@ -17,6 +17,8 @@ from openstack import resource class Account(_base.BaseResource): + _custom_metadata_prefix = "X-Account-Meta-" + base_path = "/" allow_retrieve = True diff --git a/openstack/object_store/v1/container.py b/openstack/object_store/v1/container.py index 00fdc3807..0d3cb39a4 100644 --- a/openstack/object_store/v1/container.py +++ b/openstack/object_store/v1/container.py @@ -17,6 +17,17 @@ from openstack import resource class Container(_base.BaseResource): + _custom_metadata_prefix = "X-Container-Meta-" + _system_metadata = { + "content_type": "content-type", + "detect_content_type": "x-detect-content-type", + "versions_location": "x-versions-location", + "read_ACL": "x-container-read", + "write_ACL": "x-container-write", + "sync_to": "x-container-sync-to", + "sync_key": "x-container-sync-key" + } + base_path = "/" id_attribute = "name" @@ -70,9 +81,7 @@ class Container(_base.BaseResource): #: the name before you include it in the header. To disable #: versioning, set the header to an empty string. versions_location = resource.header("x-versions-location") - #: Set to any value to disable versioning. - remove_versions_location = resource.header("x-remove-versions-location") - #: Changes the MIME type for the object. + #: The MIME type of the list of names. content_type = resource.header("content-type") #: If set to true, Object Storage guesses the content type based #: on the file extension and ignores the value sent in the diff --git a/openstack/object_store/v1/obj.py b/openstack/object_store/v1/obj.py index 86e0c9cc2..d5b10660e 100644 --- a/openstack/object_store/v1/obj.py +++ b/openstack/object_store/v1/obj.py @@ -11,12 +11,25 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from openstack import format from openstack.object_store import object_store_service +from openstack.object_store.v1 import _base from openstack import resource -class Object(resource.Resource): +class Object(_base.BaseResource): + _custom_metadata_prefix = "X-Object-Meta-" + _system_metadata = { + "content_disposition": "content-disposition", + "content_encoding": "content-encoding", + "content_type": "content-type", + "detect_content_type": "x-detect-content-type", + "delete_after": "x-delete-after", + "delete_at": "x-delete-at" + } + base_path = "/%(container)s" service = object_store_service.ObjectStoreService() id_attribute = "name" @@ -87,9 +100,6 @@ class Object(resource.Resource): content_type = resource.header("content_type", alias="content-type") #: The type of ranges that the object accepts. accept_ranges = resource.header("accept-ranges") - #: The date and time that the object was created or the last - #: time that the metadata was changed. - last_modified = resource.header("last_modified", alias="last-modified") #: 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 @@ -115,6 +125,10 @@ class Object(resource.Resource): #: which is the default. #: If not set, this header is not returned by this operation. content_disposition = resource.header("content-disposition") + #: 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. + delete_after = resource.header("x-delete-after", type=int) #: 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. @@ -125,6 +139,9 @@ class Object(resource.Resource): object_manifest = resource.header("x-object-manifest") #: The timestamp of the transaction. timestamp = resource.header("x-timestamp", type=format.UNIXEpoch) + #: The date and time that the object was created or the last + #: time that the metadata was changed. + last_modified = resource.header("last_modified", alias="last-modified") # Headers for PUT and POST requests #: Set to chunked to enable chunked transfer encoding. If used, @@ -142,21 +159,65 @@ class Object(resource.Resource): #: Using PUT with X-Copy-From has the same effect as using the #: COPY operation to copy an object. copy_from = resource.header("x-copy-from") - #: 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. - delete_after = resource.header("x-delete-after", type=int) - def get(self, session, args=None): + # The Object Store treats the metadata for its resources inconsistently so + # Object.set_metadata must override the BaseResource.set_metadata to + # account for it. + def set_metadata(self, session, metadata): + # Filter out items with empty values so the create metadata behaviour + # is the same as account and container + filtered_metadata = \ + {key: value for key, value in metadata.iteritems() if value} + + # Get a copy of the original metadata so it doesn't get erased on POST + # and update it with the new metadata values. + obj = self.head(session) + metadata2 = copy.deepcopy(obj.metadata) + metadata2.update(filtered_metadata) + + # Include any original system metadata so it doesn't get erased on POST + for key in self._system_metadata: + value = getattr(obj, key) + if value and key not in metadata2: + metadata2[key] = value + + super(Object, self).set_metadata(session, metadata2) + + # The Object Store treats the metadata for its resources inconsistently so + # Object.delete_metadata must override the BaseResource.delete_metadata to + # account for it. + def delete_metadata(self, session, keys): + # Get a copy of the original metadata so it doesn't get erased on POST + # and update it with the new metadata values. + obj = self.head(session) + metadata = copy.deepcopy(obj.metadata) + + # Include any original system metadata so it doesn't get erased on POST + for key in self._system_metadata: + value = getattr(obj, key) + if value: + metadata[key] = value + + # Remove the metadata + for key in keys: + if key == 'delete_after': + del(metadata['delete_at']) + else: + del(metadata[key]) + + url = self._get_url(self, self.id) + session.post(url, endpoint_filter=self.service, + headers=self._calculate_headers(metadata)) + + def get(self, session, include_headers=False, args=None): url = self._get_url(self, self.id) - # TODO(thowe): Add filter header support bug #1488269 headers = {'Accept': 'bytes'} resp = session.get(url, endpoint_filter=self.service, headers=headers) resp = resp.content + self._set_metadata() return resp def create(self, session): - """Create a remote resource from this instance.""" url = self._get_url(self, self.id) headers = self.get_headers() diff --git a/openstack/tests/functional/compute/v2/test_server.py b/openstack/tests/functional/compute/v2/test_server.py index dfe68454e..3c4a638d3 100644 --- a/openstack/tests/functional/compute/v2/test_server.py +++ b/openstack/tests/functional/compute/v2/test_server.py @@ -75,13 +75,18 @@ class TestServer(base.BaseFunctionalTest): self.assertDictEqual(self.conn.compute.replace_server_metadata(sot), {}) - # Insert first and last name metadata + # Create first and last name metadata meta = {"first": "Matthew", "last": "Dellavedova"} self.assertDictEqual( self.conn.compute.create_server_metadata(sot, **meta), meta) + # Create something that already exists + meta = {"last": "Inman"} + self.assertDictEqual( + self.conn.compute.create_server_metadata(sot, **meta), meta) + # Update only the first name - short = {"first": "Matt", "last": "Dellavedova"} + short = {"first": "Matt", "last": "Inman"} self.assertDictEqual( self.conn.compute.update_server_metadata(sot, first=short["first"]), diff --git a/openstack/tests/functional/object_store/v1/test_account.py b/openstack/tests/functional/object_store/v1/test_account.py new file mode 100644 index 000000000..879f15267 --- /dev/null +++ b/openstack/tests/functional/object_store/v1/test_account.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.tests.functional import base + + +class TestAccount(base.BaseFunctionalTest): + + @classmethod + def tearDownClass(cls): + super(TestAccount, cls).tearDownClass() + account = cls.conn.object_store.get_account_metadata() + cls.conn.object_store.delete_account_metadata(account.metadata.keys()) + + def test_system_metadata(self): + account = self.conn.object_store.get_account_metadata() + self.assertGreaterEqual(account.account_bytes_used, 0) + self.assertGreaterEqual(account.account_container_count, 0) + self.assertGreaterEqual(account.account_object_count, 0) + + def test_custom_metadata(self): + # get custom metadata + account = self.conn.object_store.get_account_metadata() + self.assertFalse(account.metadata) + + # set no custom metadata + self.conn.object_store.set_account_metadata() + account = self.conn.object_store.get_account_metadata() + self.assertFalse(account.metadata) + + # set empty custom metadata + self.conn.object_store.set_account_metadata(k0='') + account = self.conn.object_store.get_account_metadata() + self.assertFalse(account.metadata) + + # set custom metadata + self.conn.object_store.set_account_metadata(k1='v1') + account = self.conn.object_store.get_account_metadata() + self.assertTrue(account.metadata) + self.assertEqual(1, len(account.metadata)) + self.assertIn('k1', account.metadata) + self.assertEqual('v1', account.metadata['k1']) + + # set more custom metadata + self.conn.object_store.set_account_metadata(k2='v2') + account = self.conn.object_store.get_account_metadata() + self.assertTrue(account.metadata) + self.assertEqual(2, len(account.metadata)) + self.assertIn('k1', account.metadata) + self.assertEqual('v1', account.metadata['k1']) + self.assertIn('k2', account.metadata) + self.assertEqual('v2', account.metadata['k2']) + + # update custom metadata + self.conn.object_store.set_account_metadata(k1='v1.1') + account = self.conn.object_store.get_account_metadata() + self.assertTrue(account.metadata) + self.assertEqual(2, len(account.metadata)) + self.assertIn('k1', account.metadata) + self.assertEqual('v1.1', account.metadata['k1']) + self.assertIn('k2', account.metadata) + self.assertEqual('v2', account.metadata['k2']) + + # unset custom metadata + self.conn.object_store.delete_account_metadata(['k1']) + account = self.conn.object_store.get_account_metadata() + self.assertTrue(account.metadata) + self.assertEqual(1, len(account.metadata)) + self.assertIn('k2', account.metadata) + self.assertEqual('v2', account.metadata['k2']) diff --git a/openstack/tests/functional/object_store/v1/test_container.py b/openstack/tests/functional/object_store/v1/test_container.py index 9dc57360e..206f37211 100644 --- a/openstack/tests/functional/object_store/v1/test_container.py +++ b/openstack/tests/functional/object_store/v1/test_container.py @@ -12,7 +12,7 @@ import uuid -from openstack.object_store.v1 import container +from openstack.object_store.v1 import container as _container from openstack.tests.functional import base @@ -23,9 +23,9 @@ class TestContainer(base.BaseFunctionalTest): @classmethod def setUpClass(cls): super(TestContainer, cls).setUpClass() - tainer = cls.conn.object_store.create_container(name=cls.NAME) - assert isinstance(tainer, container.Container) - cls.assertIs(cls.NAME, tainer.name) + container = cls.conn.object_store.create_container(name=cls.NAME) + assert isinstance(container, _container.Container) + cls.assertIs(cls.NAME, container.name) @classmethod def tearDownClass(cls): @@ -37,8 +37,98 @@ class TestContainer(base.BaseFunctionalTest): names = [o.name for o in self.conn.object_store.containers()] self.assertIn(self.NAME, names) - def test_get_metadata(self): - tainer = self.conn.object_store.get_container_metadata(self.NAME) - self.assertEqual(0, tainer.object_count) - self.assertEqual(0, tainer.bytes_used) - self.assertEqual(self.NAME, tainer.name) + def test_system_metadata(self): + # get system metadata + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertEqual(0, container.object_count) + self.assertEqual(0, container.bytes_used) + + # set system metadata + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertIsNone(container.read_ACL) + self.assertIsNone(container.write_ACL) + self.conn.object_store.set_container_metadata( + container, read_ACL='.r:*', write_ACL='demo:demo') + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertEqual('.r:*', container.read_ACL) + self.assertEqual('demo:demo', container.write_ACL) + + # update system metadata + self.conn.object_store.set_container_metadata( + container, read_ACL='.r:demo') + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertEqual('.r:demo', container.read_ACL) + self.assertEqual('demo:demo', container.write_ACL) + + # set system metadata and custom metadata + self.conn.object_store.set_container_metadata( + container, k0='v0', sync_key='1234') + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertTrue(container.metadata) + self.assertIn('k0', container.metadata) + self.assertEqual('v0', container.metadata['k0']) + self.assertEqual('.r:demo', container.read_ACL) + self.assertEqual('demo:demo', container.write_ACL) + self.assertEqual('1234', container.sync_key) + + # unset system metadata + self.conn.object_store.delete_container_metadata(container, + ['sync_key']) + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertTrue(container.metadata) + self.assertIn('k0', container.metadata) + self.assertEqual('v0', container.metadata['k0']) + self.assertEqual('.r:demo', container.read_ACL) + self.assertEqual('demo:demo', container.write_ACL) + self.assertIsNone(container.sync_key) + + def test_custom_metadata(self): + # get custom metadata + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertFalse(container.metadata) + + # set no custom metadata + self.conn.object_store.set_container_metadata(container) + container = self.conn.object_store.get_container_metadata(container) + self.assertFalse(container.metadata) + + # set empty custom metadata + self.conn.object_store.set_container_metadata(container, k0='') + container = self.conn.object_store.get_container_metadata(container) + self.assertFalse(container.metadata) + + # set custom metadata + self.conn.object_store.set_container_metadata(container, k1='v1') + container = self.conn.object_store.get_container_metadata(container) + self.assertTrue(container.metadata) + self.assertEqual(1, len(container.metadata)) + self.assertIn('k1', container.metadata) + self.assertEqual('v1', container.metadata['k1']) + + # set more custom metadata by named container + self.conn.object_store.set_container_metadata(self.NAME, k2='v2') + container = self.conn.object_store.get_container_metadata(container) + self.assertTrue(container.metadata) + self.assertEqual(2, len(container.metadata)) + self.assertIn('k1', container.metadata) + self.assertEqual('v1', container.metadata['k1']) + self.assertIn('k2', container.metadata) + self.assertEqual('v2', container.metadata['k2']) + + # update metadata + self.conn.object_store.set_container_metadata(container, k1='v1.1') + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertTrue(container.metadata) + self.assertEqual(2, len(container.metadata)) + self.assertIn('k1', container.metadata) + self.assertEqual('v1.1', container.metadata['k1']) + self.assertIn('k2', container.metadata) + self.assertEqual('v2', container.metadata['k2']) + + # delete metadata + self.conn.object_store.delete_container_metadata(container, ['k1']) + container = self.conn.object_store.get_container_metadata(self.NAME) + self.assertTrue(container.metadata) + self.assertEqual(1, len(container.metadata)) + self.assertIn('k2', container.metadata) + self.assertEqual('v2', container.metadata['k2']) diff --git a/openstack/tests/functional/object_store/v1/test_obj.py b/openstack/tests/functional/object_store/v1/test_obj.py index 1ed04472b..5de3f658c 100644 --- a/openstack/tests/functional/object_store/v1/test_obj.py +++ b/openstack/tests/functional/object_store/v1/test_obj.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import uuid from openstack.tests.functional import base @@ -46,12 +47,110 @@ class TestObject(base.BaseFunctionalTest): result = self.conn.object_store.get_object(self.sot) self.assertEqual(self.DATA, result) - def test_get_metadata(self): - self.sot.data = None - self.sot.set_headers({'x-object-meta-test': 'orly'}) - result = self.conn.object_store.set_object_metadata(self.sot) - result = self.conn.object_store.get_object_metadata(self.sot) - self.assertEqual(self.FILE, result.name) - headers = result.get_headers() - self.assertEqual(str(len(self.DATA)), headers['content-length']) - self.assertEqual('orly', headers['x-object-meta-test']) + def test_system_metadata(self): + # get system metadata + obj = self.conn.object_store.get_object_metadata( + self.FILE, container=self.FOLDER) + self.assertGreaterEqual(0, obj.bytes) + self.assertIsNotNone(obj.etag) + + # set system metadata + obj = self.conn.object_store.get_object_metadata( + self.FILE, container=self.FOLDER) + self.assertIsNone(obj.content_disposition) + self.assertIsNone(obj.content_encoding) + self.conn.object_store.set_object_metadata( + obj, content_disposition='attachment', content_encoding='gzip') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertEqual('attachment', obj.content_disposition) + self.assertEqual('gzip', obj.content_encoding) + + # update system metadata + self.conn.object_store.set_object_metadata( + obj, content_encoding='deflate') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertEqual('attachment', obj.content_disposition) + self.assertEqual('deflate', obj.content_encoding) + + # set system metadata and custom metadata + self.conn.object_store.set_object_metadata( + obj, k0='v0', delete_after=100) + obj = self.conn.object_store.get_object_metadata(obj) + self.assertIn('k0', obj.metadata) + self.assertEqual('v0', obj.metadata['k0']) + self.assertEqual('attachment', obj.content_disposition) + self.assertEqual('deflate', obj.content_encoding) + self.assertIsInstance(obj.delete_at, datetime.datetime) + + # unset system metadata + self.conn.object_store.delete_object_metadata( + obj, keys=['delete_after']) + obj = self.conn.object_store.get_object_metadata(obj) + self.assertIn('k0', obj.metadata) + self.assertEqual('v0', obj.metadata['k0']) + self.assertEqual('attachment', obj.content_disposition) + self.assertEqual('deflate', obj.content_encoding) + self.assertIsNone(obj.delete_at) + + # unset more system metadata + self.conn.object_store.delete_object_metadata( + obj, keys=['content_disposition']) + obj = self.conn.object_store.get_object_metadata(obj) + self.assertIn('k0', obj.metadata) + self.assertEqual('v0', obj.metadata['k0']) + self.assertIsNone(obj.content_disposition) + self.assertEqual('deflate', obj.content_encoding) + self.assertIsNone(obj.delete_at) + + def test_custom_metadata(self): + # get custom metadata + obj = self.conn.object_store.get_object_metadata( + self.FILE, container=self.FOLDER) + self.assertFalse(obj.metadata) + + # set no custom metadata + self.conn.object_store.set_object_metadata(obj) + obj = self.conn.object_store.get_object_metadata(obj) + self.assertFalse(obj.metadata) + + # set empty custom metadata + self.conn.object_store.set_object_metadata(obj, k0='') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertFalse(obj.metadata) + + # set custom metadata + self.conn.object_store.set_object_metadata(obj, k1='v1') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertTrue(obj.metadata) + self.assertEqual(1, len(obj.metadata)) + self.assertIn('k1', obj.metadata) + self.assertEqual('v1', obj.metadata['k1']) + + # set more custom metadata by named object and container + self.conn.object_store.set_object_metadata(self.FILE, self.FOLDER, + k2='v2') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertTrue(obj.metadata) + self.assertEqual(2, len(obj.metadata)) + self.assertIn('k1', obj.metadata) + self.assertEqual('v1', obj.metadata['k1']) + self.assertIn('k2', obj.metadata) + self.assertEqual('v2', obj.metadata['k2']) + + # update custom metadata + self.conn.object_store.set_object_metadata(obj, k1='v1.1') + obj = self.conn.object_store.get_object_metadata(obj) + self.assertTrue(obj.metadata) + self.assertEqual(2, len(obj.metadata)) + self.assertIn('k1', obj.metadata) + self.assertEqual('v1.1', obj.metadata['k1']) + self.assertIn('k2', obj.metadata) + self.assertEqual('v2', obj.metadata['k2']) + + # unset custom metadata + self.conn.object_store.delete_object_metadata(obj, keys=['k1']) + obj = self.conn.object_store.get_object_metadata(obj) + self.assertTrue(obj.metadata) + self.assertEqual(1, len(obj.metadata)) + self.assertIn('k2', obj.metadata) + self.assertEqual('v2', obj.metadata['k2'])