diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample index 8d3f6c95c5..830d8d71bf 100644 --- a/etc/tempest.conf.sample +++ b/etc/tempest.conf.sample @@ -199,3 +199,18 @@ build_interval = 10 # Number of seconds to time out on waiting for a volume # to be available or reach an expected status build_timeout = 300 + +[object-storage] +# This section contains configuration options used when executing tests +# against the OpenStack Object Storage API. +# This should be the username of a user WITHOUT administrative privileges +username = admin +# The above non-administrative user's password +password = password +# The above non-administrative user's tenant name +tenant_name = admin + +# The type of endpoint for an Object Storage API service. Unless you have a +# custom Keystone service catalog implementation, you probably want to leave +# this value as "object-store" +catalog_type = object-store diff --git a/etc/tempest.conf.tpl b/etc/tempest.conf.tpl index ecb020a010..1525da860a 100644 --- a/etc/tempest.conf.tpl +++ b/etc/tempest.conf.tpl @@ -169,3 +169,18 @@ build_interval = %VOLUME_BUILD_INTERVAL% # Number of seconds to time out on waiting for a volume # to be available or reach an expected status build_timeout = %VOLUME_BUILD_TIMEOUT% + +[object-storage] +# This section contains configuration options used when executing tests +# against the OpenStack Object Storage API. +# This should be the username of a user WITHOUT administrative privileges +username = %USERNAME% +# The above non-administrative user's password +password = %PASSWORD% +# The above non-administrative user's tenant name +tenant_name = %TENANT_NAME% + +# The type of endpoint for an Object Storage API service. Unless you have a +# custom Keystone service catalog implementation, you probably want to leave +# this value as "object-store" +catalog_type = %OBJECT_CATALOG_TYPE% diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py index 798b561e60..fbe05e7646 100644 --- a/tempest/common/rest_client.py +++ b/tempest/common/rest_client.py @@ -171,6 +171,9 @@ class RestClient(object): def put(self, url, body, headers): return self.request('PUT', url, headers, body) + def head(self, url, headers=None): + return self.request('HEAD', url, headers=None) + def _log(self, req_url, body, resp, resp_body): self.log.error('Request URL: ' + req_url) self.log.error('Request Body: ' + str(body)) diff --git a/tempest/common/utils/data_utils.py b/tempest/common/utils/data_utils.py index fc7c11206c..15afd0a58e 100644 --- a/tempest/common/utils/data_utils.py +++ b/tempest/common/utils/data_utils.py @@ -1,3 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + import random import re import urllib @@ -44,3 +61,27 @@ def parse_image_id(image_ref): temp = image_ref.rsplit('/') #Return the last item, which is the image id return temp[len(temp) - 1] + + +def arbitrary_string(size=4, base_text=None): + """Return exactly size bytes worth of base_text as a string""" + + if (base_text is None) or (base_text == ''): + base_text = 'test' + + if size <= 0: + return '' + + extra = size % len(base_text) + body = '' + + if extra == 0: + body = base_text * size + + if extra == size: + body = base_text[:size] + + if extra > 0 and extra < size: + body = (size / len(base_text)) * base_text + base_text[:extra] + + return body diff --git a/tempest/config.py b/tempest/config.py index 52a4aadb95..ab8aca4c20 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -378,6 +378,31 @@ class VolumeConfig(BaseConfig): return self.get("catalog_type", 'volume') +class ObjectStorageConfig(BaseConfig): + + SECTION_NAME = "object-storage" + + @property + def username(self): + """Username to use for Object-Storage API requests.""" + return self.get("username", "admin") + + @property + def tenant_name(self): + """Tenant name to use for Object-Storage API requests.""" + return self.get("tenant_name", "admin") + + @property + def password(self): + """API key to use when authenticating.""" + return self.get("password", "password") + + @property + def catalog_type(self): + """Catalog type of the Object-Storage service.""" + return self.get("catalog_type", 'object-store') + + # TODO(jaypipes): Move this to a common utils (not data_utils...) def singleton(cls): """Simple wrapper for classes that should only have a single instance""" @@ -426,6 +451,7 @@ class TempestConfig: self.images = ImagesConfig(self._conf) self.network = NetworkConfig(self._conf) self.volume = VolumeConfig(self._conf) + self.object_storage = ObjectStorageConfig(self._conf) def load_config(self, path): """Read configuration from given path and return a config object.""" diff --git a/tempest/openstack.py b/tempest/openstack.py index c8bd238873..359e6c64f9 100644 --- a/tempest/openstack.py +++ b/tempest/openstack.py @@ -53,6 +53,9 @@ from tempest.services.nova.xml.volumes_extensions_client \ import VolumesExtensionsClientXML from tempest.services.volume.json.volumes_client import VolumesClientJSON from tempest.services.volume.xml.volumes_client import VolumesClientXML +from tempest.services.object_storage.account_client import AccountClient +from tempest.services.object_storage.container_client import ContainerClient +from tempest.services.object_storage.object_client import ObjectClient LOG = logging.getLogger(__name__) @@ -179,6 +182,9 @@ class Manager(object): raise exceptions.InvalidConfiguration(msg) self.console_outputs_client = ConsoleOutputsClient(*client_args) self.network_client = NetworkClient(*client_args) + self.account_client = AccountClient(*client_args) + self.container_client = ContainerClient(*client_args) + self.object_client = ObjectClient(*client_args) class AltManager(Manager): diff --git a/tempest/services/object_storage/__init__.py b/tempest/services/object_storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py new file mode 100644 index 0000000000..ade94d4e58 --- /dev/null +++ b/tempest/services/object_storage/account_client.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import json + +from tempest.common.rest_client import RestClient + + +class AccountClient(RestClient): + def __init__(self, config, username, password, auth_url, tenant_name=None): + super(AccountClient, self).__init__(config, username, password, + auth_url, tenant_name) + self.service = self.config.object_storage.catalog_type + self.format = 'json' + + def list_account_metadata(self): + """ + HEAD on the storage URL + Returns all account metadata headers + """ + + headers = {"X-Storage-Token", self.token} + resp, body = self.head('', headers=headers) + return resp, body + + def create_account_metadata(self, metadata, + metadata_prefix='X-Account-Meta-'): + """Creates an account metadata entry""" + headers = {} + for key in metadata: + headers[metadata_prefix + key] = metadata[key] + + resp, body = self.post('', headers=headers, body=None) + return resp, body + + def list_account_containers(self, params=None): + """ + GET on the (base) storage URL + Given the X-Storage-URL and a valid X-Auth-Token, returns + a list of all containers for the account. + + Optional Arguments: + limit=[integer value N] + Limits the number of results to at most N values + DEFAULT: 10,000 + + marker=[string value X] + Given string value X, return object names greater in value + than the specified marker. + DEFAULT: No Marker + + format=[string value, either 'json' or 'xml'] + Specify either json or xml to return the respective serialized + response. + DEFAULT: Python-List returned in response body + """ + + param_list = ['format=%s&' % self.format] + if params is not None: + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + url = '?' + ''.join(param_list) + + resp, body = self.get(url) + body = json.loads(body) + return resp, body diff --git a/tempest/services/object_storage/container_client.py b/tempest/services/object_storage/container_client.py new file mode 100644 index 0000000000..56dffde15b --- /dev/null +++ b/tempest/services/object_storage/container_client.py @@ -0,0 +1,152 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import json + +from tempest.common.rest_client import RestClient + + +class ContainerClient(RestClient): + def __init__(self, config, username, password, auth_url, tenant_name=None): + super(ContainerClient, self).__init__(config, username, password, + auth_url, tenant_name) + + #Overwrites json-specific header encoding in RestClient + self.headers = {} + self.service = self.config.object_storage.catalog_type + self.format = 'json' + + def create_container(self, container_name, metadata=None, + metadata_prefix='X-Container-Meta-'): + """ + Creates a container, with optional metadata passed in as a + dictonary + """ + url = container_name + headers = {} + + if metadata is not None: + for key in metadata: + headers[metadata_prefix + key] = metadata[key] + + resp, body = self.put(url, body=None, headers=headers) + return resp, body + + def delete_container(self, container_name): + """Deletes the container (if it's empty)""" + url = container_name + resp, body = self.delete(url) + return resp, body + + def update_container_metadata(self, container_name, metadata, + metadata_prefix='X-Container-Meta-'): + """Updates arbitrary metadata on container""" + url = container_name + headers = {} + + if metadata is not None: + for key in metadata: + headers[metadata_prefix + key] = metadata[key] + + resp, body = self.post(url, body=None, headers=headers) + + return resp. body + + def list_all_container_objects(self, container, params=None): + """ + Returns complete list of all objects in the container, even if + item count is beyond 10,000 item listing limit. + Does not require any paramaters aside from container name. + """ + #TODO: Rewite using json format to avoid newlines at end of obj names + #Set limit to API limit - 1 (max returned items = 9999) + limit = 9999 + marker = None + if params is not None: + if 'limit' in params: + limit = params['limit'] + + if 'marker' in params: + limit = params['marker'] + + resp, objlist = self.list_container_contents(container, + params={'limit': limit}) + return objlist + """tmp = [] + for obj in objlist: + tmp.append(obj['name']) + objlist = tmp + + if len(objlist) >= limit: + + #Increment marker + marker = objlist[len(objlist) - 1] + + #Get the next chunk of the list + objlist.extend(_list_all_container_objects(container, + params={'marker': marker, + 'limit': limit})) + return objlist + else: + #Return final, complete list + return objlist""" + + def list_container_contents(self, container, params=None): + """ + List the objects in a container, given the container name + + Returns the container object listing as a plain text list, or as + xml or json if that option is specified via the 'format' argument. + + Optional Arguments: + limit = integer + For an integer value n, limits the number of results to at most + n values. + + marker = 'string' + Given a string value x, return object names greater in value + than the specified marker. + + prefix = 'string' + For a string value x, causes the results to be limited to names + beginning with the substring x. + + format = 'json' or 'xml' + Specify either json or xml to return the respective serialized + response. + If json, returns a list of json objects + if xml, returns a string of xml + + path = 'string' + For a string value x, return the object names nested in the + pseudo path (assuming preconditions are met - see below). + + delimiter = 'character' + For a character c, return all the object names nested in the + container (without the need for the directory marker objects). + """ + + url = str(container) + param_list = ['format=%s&' % self.format] + if params is not None: + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + url += '?' + ''.join(param_list) + + resp, body = self.get(url) + body = json.loads(body) + return resp, body diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py new file mode 100644 index 0000000000..0fc7ad4586 --- /dev/null +++ b/tempest/services/object_storage/object_client.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import re +from tempest.common.rest_client import RestClient + + +class ObjectClient(RestClient): + def __init__(self, config, username, password, auth_url, tenant_name=None): + super(ObjectClient, self).__init__(config, username, password, + auth_url, tenant_name) + + self.service = self.config.object_storage.catalog_type + + def create_object(self, container, object_name, data): + """Create storage object""" + + url = "%s/%s" % (str(container), str(object_name)) + resp, body = self.put(url, data, self.headers) + return resp, body + + def update_object(self, container, object_name, data): + """Upload data to replace current storage object""" + return create_object(container, object_name, data) + + def delete_object(self, container, object_name): + """Delete storage object""" + url = "%s/%s" % (str(container), str(object_name)) + resp, body = self.delete(url) + return resp, body + + def update_object_metadata(self, container, object_name, metadata, + metadata_prefix='X-Object-Meta-'): + """Add, remove, or change X-Object-Meta metadata for storage object""" + + headers = {} + for key in metadata: + headers["%s%s" % (str(metadata_prefix), str(key))] = metadata[key] + + url = "%s/%s" % (str(container), str(object_name)) + resp, body = self.post(url, None, headers=headers) + return resp, body + + def list_object_metadata(self, container, object_name): + """List all storage object X-Object-Meta- metadata""" + + url = "%s/%s" % (str(container), str(object_name)) + resp, body = self.head(url) + return resp, body diff --git a/tempest/tests/object_storage/__init__.py b/tempest/tests/object_storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/tests/object_storage/base.py b/tempest/tests/object_storage/base.py new file mode 100644 index 0000000000..8edb3d2342 --- /dev/null +++ b/tempest/tests/object_storage/base.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import nose +import unittest2 as unittest + +import tempest.config +from tempest import exceptions +from tempest import openstack + + +class BaseObjectTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.os = openstack.Manager() + cls.object_client = cls.os.object_client + cls.container_client = cls.os.container_client + cls.account_client = cls.os.account_client + cls.config = cls.os.config + + try: + cls.account_client.list_account_containers() + except exceptions.EndpointNotFound: + enabled = False + skip_msg = "No OpenStack Object Storage API endpoint" + raise nose.SkipTest(skip_msg) diff --git a/tempest/tests/object_storage/test_account_services.py b/tempest/tests/object_storage/test_account_services.py new file mode 100644 index 0000000000..6d34bb8884 --- /dev/null +++ b/tempest/tests/object_storage/test_account_services.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import unittest2 as unittest +import tempest.config +import re + +from nose.plugins.attrib import attr +from tempest import exceptions +from tempest import openstack +from tempest.common.utils.data_utils import rand_name +from tempest.tests.object_storage import base + + +class AccountTest(base.BaseObjectTest): + + @classmethod + def setUpClass(cls): + super(AccountTest, cls).setUpClass() + + #Create a container + cls.container_name = rand_name(name='TestContainer') + cls.container_client.create_container(cls.container_name) + + @classmethod + def tearDownClass(cls): + cls.container_client.delete_container(cls.container_name) + + @attr(type='smoke') + def test_list_containers(self): + """List of all containers should not be empty""" + + params = {'format': 'json'} + resp, container_list = \ + self.account_client.list_account_containers(params=params) + + self.assertIsNotNone(container_list) + container_names = [c['name'] for c in container_list] + self.assertTrue(self.container_name in container_names) + + @attr(type='smoke') + def test_list_account_metadata(self): + """List all account metadata""" + + resp, metadata = self.account_client.list_account_metadata() + self.assertEqual(resp['status'], '204') + self.assertIn('x-account-object-count', resp) + self.assertIn('x-account-container-count', resp) + self.assertIn('x-account-bytes-used', resp) + + @attr(type='smoke') + def test_create_account_metadata(self): + """Add metadata to account""" + + metadata = {'test-account-meta': 'Meta!'} + resp, _ = \ + self.account_client.create_account_metadata(metadata=metadata) + self.assertEqual(resp['status'], '204') + + resp, metadata = self.account_client.list_account_metadata() + self.assertIn('x-account-meta-test-account-meta', resp) + self.assertEqual(resp['x-account-meta-test-account-meta'], 'Meta!') diff --git a/tempest/tests/object_storage/test_container_services.py b/tempest/tests/object_storage/test_container_services.py new file mode 100644 index 0000000000..639698b1f9 --- /dev/null +++ b/tempest/tests/object_storage/test_container_services.py @@ -0,0 +1,110 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import re +import unittest2 as unittest +import tempest.config + +from nose.plugins.attrib import attr +from tempest import exceptions +from tempest import openstack +from tempest.common.utils.data_utils import rand_name, arbitrary_string +from tempest.tests.object_storage import base + + +class ContainerTest(base.BaseObjectTest): + + @classmethod + def setUpClass(cls): + super(ContainerTest, cls).setUpClass() + cls.containers = [] + + @classmethod + def tearDownClass(cls): + for container in cls.containers: + #Get list of all object in the container + objlist = \ + cls.container_client.list_all_container_objects(container) + + #Attempt to delete every object in the container + for obj in objlist: + resp, _ = \ + cls.object_client.delete_object(container, obj['name']) + + #Attempt to delete the container + resp, _ = cls.container_client.delete_container(container) + + @attr(type='smoke') + def test_create_container(self): + """Create a container, test responses""" + + #Create a container + container_name = rand_name(name='TestContainer') + resp, body = self.container_client.create_container(container_name) + self.containers.append(container_name) + + self.assertTrue(resp['status'] in ('202', '201')) + + @attr(type='smoke') + def test_delete_container(self): + """Create and Delete a container, test responses""" + + #Create a container + container_name = rand_name(name='TestContainer') + resp, _ = self.container_client.create_container(container_name) + self.containers.append(container_name) + + #Delete Container + resp, _ = self.container_client.delete_container(container_name) + self.assertEqual(resp['status'], '204') + self.containers.remove(container_name) + + @attr(type='smoke') + def test_list_container_contents_json(self): + """Add metadata to object""" + + #Create a container + container_name = rand_name(name='TestContainer') + resp, _ = self.container_client.create_container(container_name) + self.containers.append(container_name) + + #Create Object + object_name = rand_name(name='TestObject') + data = arbitrary_string() + resp, _ = self.object_client.create_object(container_name, + object_name, data) + + #Set Object Metadata + meta_key = rand_name(name='Meta-Test-') + meta_value = rand_name(name='MetaValue-') + orig_metadata = {meta_key: meta_value} + + resp, _ = self.object_client.update_object_metadata(container_name, + object_name, + orig_metadata) + + #Get Container contents list json format + params = {'format': 'json'} + resp, object_list = \ + self.container_client.\ + list_container_contents(container_name, params=params) + + self.assertEqual(resp['status'], '200') + self.assertIsNotNone(object_list) + + object_names = [obj['name'] for obj in object_list] + self.assertIn(object_name, object_names) diff --git a/tempest/tests/object_storage/test_object_services.py b/tempest/tests/object_storage/test_object_services.py new file mode 100644 index 0000000000..ab92d26f9a --- /dev/null +++ b/tempest/tests/object_storage/test_object_services.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack, LLC +# All Rights Reserved. +# +# 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. + +import re +import unittest2 as unittest +import tempest.config + +from nose.plugins.attrib import attr +from tempest import exceptions +from tempest import openstack +from tempest.common.utils.data_utils import rand_name, arbitrary_string +from tempest.tests.object_storage import base + + +class ObjectTest(base.BaseObjectTest): + + @classmethod + def setUpClass(cls): + super(ObjectTest, cls).setUpClass() + + #Create a container + cls.container_name = rand_name(name='TestContainer') + cls.container_client.create_container(cls.container_name) + + @classmethod + def tearDownClass(cls): + #Get list of all object in the container + objlist = \ + cls.container_client.list_all_container_objects(cls.container_name) + + #Attempt to delete every object in the container + for obj in objlist: + resp, _ = cls.object_client.delete_object(cls.container_name, + obj['name']) + + #Attempt to delete the container + resp, _ = cls.container_client.delete_container(cls.container_name) + + @attr(type='smoke') + def test_create_object(self): + """Create storage object, test response""" + + #Create Object + object_name = rand_name(name='TestObject') + data = arbitrary_string() + resp, _ = self.object_client.create_object(self.container_name, + object_name, data) + + #Create another Object + object_name = rand_name(name='TestObject') + data = arbitrary_string() + resp, _ = self.object_client.create_object(self.container_name, + object_name, data) + self.assertEqual(resp['status'], '201') + + @attr(type='smoke') + def test_delete_object(self): + """Create and delete a storage object, test responses""" + + #Create Object + object_name = rand_name(name='TestObject') + data = arbitrary_string() + resp, _ = self.object_client.create_object(self.container_name, + object_name, data) + + resp, _ = self.object_client.delete_object(self.container_name, + object_name) + self.assertEqual(resp['status'], '204') + + @attr(type='smoke') + def test_object_metadata(self): + """Add metadata to storage object, test if metadata is retrievable""" + + #Create Object + object_name = rand_name(name='TestObject') + data = arbitrary_string() + resp, _ = self.object_client.create_object(self.container_name, + object_name, data) + + #Set Object Metadata + meta_key = rand_name(name='test-') + meta_value = rand_name(name='MetaValue-') + orig_metadata = {meta_key: meta_value} + + resp, _ = \ + self.object_client.update_object_metadata(self.container_name, + object_name, + orig_metadata) + self.assertEqual(resp['status'], '202') + + #Get Object Metadata + resp, resp_metadata = \ + self.object_client.list_object_metadata(self.container_name, + object_name) + self.assertEqual(resp['status'], '200') + actual_meta_key = 'x-object-meta-' + meta_key + self.assertTrue(actual_meta_key in resp) + self.assertEqual(resp[actual_meta_key], meta_value)