Initial add of Swift tests

* Added Swift client
* Added Swift configuration
* Added basic Swift tests for objects, containers, and accounts

Change-Id: I92ac9f69f3e8fe227dff55a2a5b55001f37acb73
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 8d3f6c9..830d8d7 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -199,3 +199,18 @@
 # 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 ecb020a..1525da8 100644
--- a/etc/tempest.conf.tpl
+++ b/etc/tempest.conf.tpl
@@ -169,3 +169,18 @@
 # 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 71f887f..55baa8a 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -171,6 +171,9 @@
     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 fc7c112..15afd0a 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 @@
     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 52a4aad..ab8aca4 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -378,6 +378,31 @@
         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 @@
         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 c8bd238..359e6c6 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -53,6 +53,9 @@
 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 @@
             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 0000000..e69de29
--- /dev/null
+++ b/tempest/services/object_storage/__init__.py
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
new file mode 100644
index 0000000..ade94d4
--- /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 0000000..56dffde
--- /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 0000000..0fc7ad4
--- /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 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/object_storage/__init__.py
diff --git a/tempest/tests/object_storage/base.py b/tempest/tests/object_storage/base.py
new file mode 100644
index 0000000..8edb3d2
--- /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 0000000..6d34bb8
--- /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 0000000..639698b
--- /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 0000000..ab92d26
--- /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)