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
This commit is contained in:
Everett Toews 2016-02-02 17:19:06 -06:00
parent 3265aa3e8d
commit 4ae0638048
9 changed files with 555 additions and 105 deletions

View File

@ -18,6 +18,52 @@ from openstack import resource
class BaseResource(resource.Resource): class BaseResource(resource.Resource):
service = object_store_service.ObjectStoreService() 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 @classmethod
def update_by_id(cls, session, resource_id, attrs, path_args=None): def update_by_id(cls, session, resource_id, attrs, path_args=None):
"""Update a Resource with the given attributes. """Update a Resource with the given attributes.

View File

@ -19,30 +19,36 @@ from openstack import proxy
class Proxy(proxy.BaseProxy): class Proxy(proxy.BaseProxy):
def get_account_metadata(self): def get_account_metadata(self):
"""Get metatdata for this account """Get metadata for this account.
:rtype: :rtype:
:class:`~openstack.object_store.v1.account.Account` :class:`~openstack.object_store.v1.account.Account`
""" """
return self._head(_account.Account) return self._head(_account.Account)
def set_account_metadata(self, account): def set_account_metadata(self, **metadata):
"""Set metatdata for this account. """Set metadata for this account.
:param account: Account metadata specified on a :param kwargs metadata: Key/value pairs to be set as metadata
:class:`~openstack.object_store.v1.account.Account` object on the container. Custom metadata can be set.
to be sent to the server. Custom metadata are keys and values defined
:type account: by the user.
:class:`~openstack.object_store.v1.account.Account`
:rtype: ``None``
""" """
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): def containers(self, **query):
"""Obtain Container objects for this account. """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. the resources being returned.
:rtype: A generator of :rtype: A generator of
@ -50,30 +56,6 @@ class Proxy(proxy.BaseProxy):
""" """
return _container.Container.list(self.session, **query) 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): def create_container(self, **attrs):
"""Create a new container from attributes """Create a new container from attributes
@ -103,6 +85,54 @@ class Proxy(proxy.BaseProxy):
self._delete(_container.Container, container, self._delete(_container.Container, container,
ignore_missing=ignore_missing) 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): def objects(self, container, **query):
"""Return a generator that yields the Container's objects. """Return a generator that yields the Container's objects.
@ -121,28 +151,24 @@ class Proxy(proxy.BaseProxy):
objs = _obj.Object.list(self.session, objs = _obj.Object.list(self.session,
path_args={"container": container.name}, path_args={"container": container.name},
**query) **query)
# TODO(briancurtin): Objects have to know their container at this for obj in objs:
# point, otherwise further operations like getting their metadata obj.container = container.name
# or downloading them is a hassle because the end-user would have yield obj
# to maintain both the container and the object separately.
for ob in objs:
ob.container = container.name
yield ob
def _get_container_name(self, object, container): def _get_container_name(self, obj, container):
if isinstance(object, _obj.Object): if isinstance(obj, _obj.Object):
if object.container is not None: if obj.container is not None:
return object.container return obj.container
if container is not None: if container is not None:
container = _container.Container.from_id(container) container = _container.Container.from_id(container)
return container.name return container.name
raise ValueError("container must be specified") 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 """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. :class:`~openstack.object_store.v1.obj.Object` instance.
:param container: The value can be the name of a container or a :param container: The value can be the name of a container or a
:class:`~openstack.object_store.v1.container.Container` :class:`~openstack.object_store.v1.container.Container`
@ -154,15 +180,15 @@ class Proxy(proxy.BaseProxy):
:raises: :class:`~openstack.exceptions.ResourceNotFound` :raises: :class:`~openstack.exceptions.ResourceNotFound`
when no resource can be found. 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}) 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. """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. :class:`~openstack.object_store.v1.obj.Object` instance.
:param container: The value can be the name of a container or a :param container: The value can be the name of a container or a
:class:`~openstack.object_store.v1.container.Container` :class:`~openstack.object_store.v1.container.Container`
@ -173,7 +199,7 @@ class Proxy(proxy.BaseProxy):
when no resource can be found. when no resource can be found.
""" """
with open(path, "w") as out: 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): def upload_object(self, **attrs):
"""Upload a new object from attributes """Upload a new object from attributes
@ -199,10 +225,10 @@ class Proxy(proxy.BaseProxy):
"""Copy an object.""" """Copy an object."""
raise NotImplementedError 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 """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` :class:`~openstack.object_store.v1.container.Container`
instance. instance.
:param container: The value can be the ID of a container or a :param container: The value can be the ID of a container or a
@ -216,17 +242,16 @@ class Proxy(proxy.BaseProxy):
:returns: ``None`` :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}) path_args={"container": container_name})
def get_object_metadata(self, object, container=None): def get_object_metadata(self, obj, container=None):
"""Get metatdata for an object """Get metadata for an object.
:param object: The value is an :param obj: The value can be the name of an object or a
:class:`~openstack.object_store.v1.obj.Object` :class:`~openstack.object_store.v1.obj.Object` instance.
instance.
:param container: The value can be the ID of a container or a :param container: The value can be the ID of a container or a
:class:`~openstack.object_store.v1.container.Container` :class:`~openstack.object_store.v1.container.Container`
instance. instance.
@ -235,17 +260,51 @@ class Proxy(proxy.BaseProxy):
:raises: :class:`~openstack.exceptions.ResourceNotFound` :raises: :class:`~openstack.exceptions.ResourceNotFound`
when no resource can be found. 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}) path_args={"container": container_name})
def set_object_metadata(self, object): def set_object_metadata(self, obj, container=None, **metadata):
"""Set metatdata for an object. """Set metadata for an object.
:param object: The object to set metadata for. Note: This method will do an extra HEAD call.
:type object: :class:`~openstack.object_store.v1.obj.Object`
: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)

View File

@ -17,6 +17,8 @@ from openstack import resource
class Account(_base.BaseResource): class Account(_base.BaseResource):
_custom_metadata_prefix = "X-Account-Meta-"
base_path = "/" base_path = "/"
allow_retrieve = True allow_retrieve = True

View File

@ -17,6 +17,17 @@ from openstack import resource
class Container(_base.BaseResource): 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 = "/" base_path = "/"
id_attribute = "name" id_attribute = "name"
@ -70,9 +81,7 @@ class Container(_base.BaseResource):
#: the name before you include it in the header. To disable #: the name before you include it in the header. To disable
#: versioning, set the header to an empty string. #: versioning, set the header to an empty string.
versions_location = resource.header("x-versions-location") versions_location = resource.header("x-versions-location")
#: Set to any value to disable versioning. #: The MIME type of the list of names.
remove_versions_location = resource.header("x-remove-versions-location")
#: Changes the MIME type for the object.
content_type = resource.header("content-type") content_type = resource.header("content-type")
#: If set to true, Object Storage guesses the content type based #: If set to true, Object Storage guesses the content type based
#: on the file extension and ignores the value sent in the #: on the file extension and ignores the value sent in the

View File

@ -11,12 +11,25 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
from openstack import format from openstack import format
from openstack.object_store import object_store_service from openstack.object_store import object_store_service
from openstack.object_store.v1 import _base
from openstack import resource 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" base_path = "/%(container)s"
service = object_store_service.ObjectStoreService() service = object_store_service.ObjectStoreService()
id_attribute = "name" id_attribute = "name"
@ -87,9 +100,6 @@ class Object(resource.Resource):
content_type = resource.header("content_type", alias="content-type") content_type = resource.header("content_type", alias="content-type")
#: The type of ranges that the object accepts. #: The type of ranges that the object accepts.
accept_ranges = resource.header("accept-ranges") 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 #: For objects smaller than 5 GB, this value is the MD5 checksum
#: of the object content. The value is not quoted. #: of the object content. The value is not quoted.
#: For manifest objects, this value is the MD5 checksum of the #: For manifest objects, this value is the MD5 checksum of the
@ -115,6 +125,10 @@ class Object(resource.Resource):
#: which is the default. #: which is the default.
#: If not set, this header is not returned by this operation. #: If not set, this header is not returned by this operation.
content_disposition = resource.header("content-disposition") 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 #: If set, the time when the object will be deleted by the system
#: in the format of a UNIX Epoch timestamp. #: in the format of a UNIX Epoch timestamp.
#: If not set, this header is not returned by this operation. #: 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") object_manifest = resource.header("x-object-manifest")
#: The timestamp of the transaction. #: The timestamp of the transaction.
timestamp = resource.header("x-timestamp", type=format.UNIXEpoch) 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 # Headers for PUT and POST requests
#: Set to chunked to enable chunked transfer encoding. If used, #: 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 #: Using PUT with X-Copy-From has the same effect as using the
#: COPY operation to copy an object. #: COPY operation to copy an object.
copy_from = resource.header("x-copy-from") 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) url = self._get_url(self, self.id)
# TODO(thowe): Add filter header support bug #1488269
headers = {'Accept': 'bytes'} headers = {'Accept': 'bytes'}
resp = session.get(url, endpoint_filter=self.service, headers=headers) resp = session.get(url, endpoint_filter=self.service, headers=headers)
resp = resp.content resp = resp.content
self._set_metadata()
return resp return resp
def create(self, session): def create(self, session):
"""Create a remote resource from this instance."""
url = self._get_url(self, self.id) url = self._get_url(self, self.id)
headers = self.get_headers() headers = self.get_headers()

View File

@ -75,13 +75,18 @@ class TestServer(base.BaseFunctionalTest):
self.assertDictEqual(self.conn.compute.replace_server_metadata(sot), 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"} meta = {"first": "Matthew", "last": "Dellavedova"}
self.assertDictEqual( self.assertDictEqual(
self.conn.compute.create_server_metadata(sot, **meta), meta) 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 # Update only the first name
short = {"first": "Matt", "last": "Dellavedova"} short = {"first": "Matt", "last": "Inman"}
self.assertDictEqual( self.assertDictEqual(
self.conn.compute.update_server_metadata(sot, self.conn.compute.update_server_metadata(sot,
first=short["first"]), first=short["first"]),

View File

@ -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'])

View File

@ -12,7 +12,7 @@
import uuid import uuid
from openstack.object_store.v1 import container from openstack.object_store.v1 import container as _container
from openstack.tests.functional import base from openstack.tests.functional import base
@ -23,9 +23,9 @@ class TestContainer(base.BaseFunctionalTest):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestContainer, cls).setUpClass() super(TestContainer, cls).setUpClass()
tainer = cls.conn.object_store.create_container(name=cls.NAME) container = cls.conn.object_store.create_container(name=cls.NAME)
assert isinstance(tainer, container.Container) assert isinstance(container, _container.Container)
cls.assertIs(cls.NAME, tainer.name) cls.assertIs(cls.NAME, container.name)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
@ -37,8 +37,98 @@ class TestContainer(base.BaseFunctionalTest):
names = [o.name for o in self.conn.object_store.containers()] names = [o.name for o in self.conn.object_store.containers()]
self.assertIn(self.NAME, names) self.assertIn(self.NAME, names)
def test_get_metadata(self): def test_system_metadata(self):
tainer = self.conn.object_store.get_container_metadata(self.NAME) # get system metadata
self.assertEqual(0, tainer.object_count) container = self.conn.object_store.get_container_metadata(self.NAME)
self.assertEqual(0, tainer.bytes_used) self.assertEqual(0, container.object_count)
self.assertEqual(self.NAME, tainer.name) 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'])

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import datetime
import uuid import uuid
from openstack.tests.functional import base from openstack.tests.functional import base
@ -46,12 +47,110 @@ class TestObject(base.BaseFunctionalTest):
result = self.conn.object_store.get_object(self.sot) result = self.conn.object_store.get_object(self.sot)
self.assertEqual(self.DATA, result) self.assertEqual(self.DATA, result)
def test_get_metadata(self): def test_system_metadata(self):
self.sot.data = None # get system metadata
self.sot.set_headers({'x-object-meta-test': 'orly'}) obj = self.conn.object_store.get_object_metadata(
result = self.conn.object_store.set_object_metadata(self.sot) self.FILE, container=self.FOLDER)
result = self.conn.object_store.get_object_metadata(self.sot) self.assertGreaterEqual(0, obj.bytes)
self.assertEqual(self.FILE, result.name) self.assertIsNotNone(obj.etag)
headers = result.get_headers()
self.assertEqual(str(len(self.DATA)), headers['content-length']) # set system metadata
self.assertEqual('orly', headers['x-object-meta-test']) 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'])