Merge "Initial add of Swift tests"

This commit is contained in:
Jenkins 2012-10-11 23:26:08 +00:00 committed by Gerrit Code Review
commit d246eb4350
15 changed files with 740 additions and 0 deletions

View File

@ -199,3 +199,18 @@ build_interval = 10
# Number of seconds to time out on waiting for a volume # Number of seconds to time out on waiting for a volume
# to be available or reach an expected status # to be available or reach an expected status
build_timeout = 300 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

View File

@ -169,3 +169,18 @@ build_interval = %VOLUME_BUILD_INTERVAL%
# Number of seconds to time out on waiting for a volume # Number of seconds to time out on waiting for a volume
# to be available or reach an expected status # to be available or reach an expected status
build_timeout = %VOLUME_BUILD_TIMEOUT% 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%

View File

@ -171,6 +171,9 @@ class RestClient(object):
def put(self, url, body, headers): def put(self, url, body, headers):
return self.request('PUT', url, headers, body) 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): def _log(self, req_url, body, resp, resp_body):
self.log.error('Request URL: ' + req_url) self.log.error('Request URL: ' + req_url)
self.log.error('Request Body: ' + str(body)) self.log.error('Request Body: ' + str(body))

View File

@ -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 random
import re import re
import urllib import urllib
@ -44,3 +61,27 @@ def parse_image_id(image_ref):
temp = image_ref.rsplit('/') temp = image_ref.rsplit('/')
#Return the last item, which is the image id #Return the last item, which is the image id
return temp[len(temp) - 1] 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

View File

@ -378,6 +378,31 @@ class VolumeConfig(BaseConfig):
return self.get("catalog_type", 'volume') 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...) # TODO(jaypipes): Move this to a common utils (not data_utils...)
def singleton(cls): def singleton(cls):
"""Simple wrapper for classes that should only have a single instance""" """Simple wrapper for classes that should only have a single instance"""
@ -426,6 +451,7 @@ class TempestConfig:
self.images = ImagesConfig(self._conf) self.images = ImagesConfig(self._conf)
self.network = NetworkConfig(self._conf) self.network = NetworkConfig(self._conf)
self.volume = VolumeConfig(self._conf) self.volume = VolumeConfig(self._conf)
self.object_storage = ObjectStorageConfig(self._conf)
def load_config(self, path): def load_config(self, path):
"""Read configuration from given path and return a config object.""" """Read configuration from given path and return a config object."""

View File

@ -53,6 +53,9 @@ from tempest.services.nova.xml.volumes_extensions_client \
import VolumesExtensionsClientXML import VolumesExtensionsClientXML
from tempest.services.volume.json.volumes_client import VolumesClientJSON from tempest.services.volume.json.volumes_client import VolumesClientJSON
from tempest.services.volume.xml.volumes_client import VolumesClientXML 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__) LOG = logging.getLogger(__name__)
@ -179,6 +182,9 @@ class Manager(object):
raise exceptions.InvalidConfiguration(msg) raise exceptions.InvalidConfiguration(msg)
self.console_outputs_client = ConsoleOutputsClient(*client_args) self.console_outputs_client = ConsoleOutputsClient(*client_args)
self.network_client = NetworkClient(*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): class AltManager(Manager):

View File

@ -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

View File

@ -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

View File

@ -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

View File

View File

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

View File

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

View File

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

View File

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