Merge "Improve logging of credentials"
diff --git a/HACKING.rst b/HACKING.rst
index b66fa24..d3ac5c6 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -132,16 +132,16 @@
Set-up is split in a series of steps (setup stages), which can be overwritten
by test classes. Set-up stages are:
-- `skip_checks`
-- `setup_credentials`
-- `setup_clients`
-- `resource_setup`
+ - `skip_checks`
+ - `setup_credentials`
+ - `setup_clients`
+ - `resource_setup`
Tear-down is also split in a series of steps (teardown stages), which are
stacked for execution only if the corresponding setup stage had been
reached during the setup phase. Tear-down stages are:
-- `clear_credentials` (defined in the base test class)
-- `resource_cleanup`
+ - `clear_credentials` (defined in the base test class)
+ - `resource_cleanup`
Skipping Tests
--------------
diff --git a/releasenotes/notes/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml b/releasenotes/notes/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml
new file mode 100644
index 0000000..9baf035
--- /dev/null
+++ b/releasenotes/notes/12.0.0-supported-openstack-releases-f10aac381d933dd1.yaml
@@ -0,0 +1,12 @@
+---
+prelude: >
+ This release is marking the end of Kilo release support in Tempest
+other:
+ - OpenStack Releases Supported after this release are **Liberty**
+ and **Mitaka**
+
+ The release under current development as of this tag is Newton,
+ meaning that every Tempest commit is also tested against master during
+ the Newton cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Newton (or future releases)
+ cloud.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index b617b22..2c22408 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -5,6 +5,7 @@
.. toctree::
:maxdepth: 1
+ v12.0.0
v11.0.0
v10.0.0
unreleased
diff --git a/releasenotes/source/v12.0.0.rst b/releasenotes/source/v12.0.0.rst
new file mode 100644
index 0000000..0bc8343
--- /dev/null
+++ b/releasenotes/source/v12.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v12.0.0 Release Notes
+=====================
+
+.. release-notes:: 12.0.0 Release Notes
+ :version: 12.0.0
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 1a84d06..cad22f3 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -32,7 +32,7 @@
if container_format in a_formats and container_format != disk_format:
msg = ("The container format and the disk format don't match. "
- "Contaiter format: %(container)s, Disk format: %(disk)s." %
+ "Container format: %(container)s, Disk format: %(disk)s." %
{'container': container_format, 'disk': disk_format})
raise exceptions.InvalidConfiguration(message=msg)
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index e8b035b..dc926e0 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -179,23 +179,14 @@
@test.idempotent_id('84dafe57-9666-4f6d-84c8-0814d37923b8')
def test_create_object_with_expect_continue(self):
# create object with expect_continue
+
object_name = data_utils.rand_name(name='TestObject')
data = data_utils.arbitrary_string()
- metadata = {'Expect': '100-continue'}
- resp = self.object_client.create_object_continue(
- self.container_name,
- object_name,
- data,
- metadata=metadata)
- self.assertIn('status', resp)
- self.assertEqual(resp['status'], '100')
+ status, _ = self.object_client.create_object_continue(
+ self.container_name, object_name, data)
- self.object_client.create_object_continue(
- self.container_name,
- object_name,
- data,
- metadata=None)
+ self.assertEqual(status, 201)
# check uploaded content
_, body = self.object_client.get_object(self.container_name,
diff --git a/tempest/api/volume/admin/test_volume_pools.py b/tempest/api/volume/admin/test_volume_pools.py
new file mode 100644
index 0000000..c662e8c
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_pools.py
@@ -0,0 +1,43 @@
+# Copyright 2016 OpenStack Foundation
+# 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.
+
+from tempest.api.volume import base
+from tempest import test
+
+
+class VolumePoolsAdminV2TestsJSON(base.BaseVolumeAdminTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumePoolsAdminV2TestsJSON, cls).resource_setup()
+ # Create a test shared volume for tests
+ cls.volume = cls.create_volume()
+
+ @test.idempotent_id('0248a46c-e226-4933-be10-ad6fca8227e7')
+ def test_get_pools_without_details(self):
+ volume_info = self.admin_volume_client. \
+ show_volume(self.volume['id'])['volume']
+ cinder_pools = self.admin_volume_client.show_pools()['pools']
+ self.assertIn(volume_info['os-vol-host-attr:host'],
+ [pool['name'] for pool in cinder_pools])
+
+ @test.idempotent_id('d4bb61f7-762d-4437-b8a4-5785759a0ced')
+ def test_get_pools_with_details(self):
+ volume_info = self.admin_volume_client. \
+ show_volume(self.volume['id'])['volume']
+ cinder_pools = self.admin_volume_client.\
+ show_pools(detail=True)['pools']
+ self.assertIn(volume_info['os-vol-host-attr:host'],
+ [pool['name'] for pool in cinder_pools])
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 866e676..6707121 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -14,6 +14,7 @@
from tempest.common.utils import data_utils
from tempest.common import waiters
from tempest import config
+from tempest.lib import decorators
from tempest import test
CONF = config.CONF
@@ -34,6 +35,9 @@
cls.name_field = cls.special_fields['name_field']
cls.descrip_field = cls.special_fields['descrip_field']
+ # Create 2 snapshots
+ for _ in xrange(2):
+ cls.create_snapshot(cls.volume_origin['id'])
def _detach(self, volume_id):
"""Detach volume."""
@@ -58,6 +62,14 @@
('details' if with_detail else '', key)
self.assertEqual(params[key], snap[key], msg)
+ def _list_snapshots_by_param_limit(self, limit, expected_elements):
+ """list snapshots by limit param"""
+ # Get snapshots list using limit parameter
+ fetched_snap_list = self.snapshots_client.list_snapshots(
+ limit=limit)['snapshots']
+ # Validating filtered snapshots length equals to expected_elements
+ self.assertEqual(expected_elements, len(fetched_snap_list))
+
@test.idempotent_id('b467b54c-07a4-446d-a1cf-651dedcc3ff1')
@test.services('compute')
def test_snapshot_create_with_volume_in_use(self):
@@ -178,6 +190,25 @@
self.volumes_client.wait_for_resource_deletion(volume['id'])
self.cleanup_snapshot(snapshot)
+ @test.idempotent_id('db4d8e0a-7a2e-41cc-a712-961f6844e896')
+ def test_snapshot_list_param_limit(self):
+ # List returns limited elements
+ self._list_snapshots_by_param_limit(limit=1, expected_elements=1)
+
+ @test.idempotent_id('a1427f61-420e-48a5-b6e3-0b394fa95400')
+ def test_snapshot_list_param_limit_equals_infinite(self):
+ # List returns all elements when request limit exceeded
+ # snapshots number
+ snap_list = self.snapshots_client.list_snapshots()['snapshots']
+ self._list_snapshots_by_param_limit(limit=100000,
+ expected_elements=len(snap_list))
+
+ @decorators.skip_because(bug='1540893')
+ @test.idempotent_id('e3b44b7f-ae87-45b5-8a8c-66110eb24d0a')
+ def test_snapshot_list_param_limit_equals_zero(self):
+ # List returns zero elements
+ self._list_snapshots_by_param_limit(limit=0, expected_elements=0)
+
def cleanup_snapshot(self, snapshot):
# Delete the snapshot
self.snapshots_client.delete_snapshot(snapshot['id'])
diff --git a/tempest/common/dynamic_creds.py b/tempest/common/dynamic_creds.py
index d374be4..a63af38 100644
--- a/tempest/common/dynamic_creds.py
+++ b/tempest/common/dynamic_creds.py
@@ -154,6 +154,20 @@
return cred_provider.TestResources(creds)
def _create_network_resources(self, tenant_id):
+ """The function creates network resources in the given tenant.
+
+ The function checks if network_resources class member is empty,
+ In case it is, it will create a network, a subnet and a router for
+ the tenant according to the given tenant id parameter.
+ Otherwise it will create a network resource according
+ to the values from network_resources dict.
+
+ :param tenant_id: The tenant id to create resources for.
+ :type tenant_id: str
+ :raises: InvalidConfiguration, Exception
+ :returns: network resources(network,subnet,router)
+ :rtype: tuple
+ """
network = None
subnet = None
router = None
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index 4b932ce..504d72b 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -31,7 +31,7 @@
"""The test suite for server advanced operations
This test case stresses some advanced server instance operations:
- * Resizing an instance
+ * Resizing a volume-backed instance
* Sequence suspend resume
"""
@@ -50,10 +50,10 @@
@test.idempotent_id('e6c28180-7454-4b59-b188-0257af08a63b')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize is not available.')
- @test.services('compute')
- def test_resize_server_confirm(self):
+ @test.services('compute', 'volume')
+ def test_resize_volume_backed_server_confirm(self):
# We create an instance for use in this test
- instance = self.create_server(wait_until='ACTIVE')
+ instance = self.create_server(wait_until='ACTIVE', volume_backed=True)
instance_id = instance['id']
resize_flavor = CONF.compute.flavor_ref_alt
LOG.debug("Resizing instance %s from flavor %s to flavor %s",
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 0acd4ad..fa43d94 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -18,6 +18,7 @@
from six.moves.urllib import parse as urlparse
from tempest.lib.common import rest_client
+from tempest.lib import exceptions
class ObjectClient(rest_client.RestClient):
@@ -170,29 +171,67 @@
def create_object_continue(self, container, object_name,
data, metadata=None):
- """Create storage object."""
+ """Put an object using Expect:100-continue"""
headers = {}
if metadata:
for key in metadata:
headers[str(key)] = metadata[key]
- if not data:
- headers['content-length'] = '0'
-
headers['X-Auth-Token'] = self.token
+ headers['content-length'] = 0 if data is None else len(data)
+ headers['Expect'] = '100-continue'
- conn = put_object_connection(self.base_url, str(container),
- str(object_name), data, None, headers)
+ parsed = urlparse.urlparse(self.base_url)
+ path = str(parsed.path) + "/"
+ path += "%s/%s" % (str(container), str(object_name))
+ conn = create_connection(parsed)
+
+ # Send the PUT request and the headers including the "Expect" header
+ conn.putrequest('PUT', path)
+
+ for header, value in six.iteritems(headers):
+ conn.putheader(header, value)
+ conn.endheaders()
+
+ # Read the 100 status prior to sending the data
response = conn.response_class(conn.sock,
strict=conn.strict,
method=conn._method)
- version, status, reason = response._read_status()
- resp = {'version': version,
- 'status': str(status),
- 'reason': reason}
+ _, status, _ = response._read_status()
- return resp
+ # toss the CRLF at the end of the response
+ response._safe_read(2)
+
+ # Expecting a 100 here, if not close and throw an exception
+ if status != 100:
+ conn.close()
+ pattern = "%s %s" % (
+ """Unexpected http success status code {0}.""",
+ """The expected status code is {1}""")
+ details = pattern.format(status, 100)
+ raise exceptions.UnexpectedResponseCode(details)
+
+ # If a continue was received go ahead and send the data
+ # and get the final response
+ conn.send(data)
+
+ resp = conn.getresponse()
+
+ return resp.status, resp.reason
+
+
+def create_connection(parsed_url):
+ """Helper function to create connection with httplib
+
+ :param parsed_url: parsed url of the remote location
+ """
+ if parsed_url.scheme == 'https':
+ conn = httplib.HTTPSConnection(parsed_url.netloc)
+ else:
+ conn = httplib.HTTPConnection(parsed_url.netloc)
+
+ return conn
def put_object_connection(base_url, container, name, contents=None,
@@ -211,13 +250,12 @@
:param query_string: if set will be appended with '?' to generated path
"""
parsed = urlparse.urlparse(base_url)
- if parsed.scheme == 'https':
- conn = httplib.HTTPSConnection(parsed.netloc)
- else:
- conn = httplib.HTTPConnection(parsed.netloc)
+
path = str(parsed.path) + "/"
path += "%s/%s" % (str(container), str(name))
+ conn = create_connection(parsed)
+
if query_string:
path += '?' + query_string
if headers:
diff --git a/tempest/services/volume/base/base_volumes_client.py b/tempest/services/volume/base/base_volumes_client.py
index 4344802..6237745 100644
--- a/tempest/services/volume/base/base_volumes_client.py
+++ b/tempest/services/volume/base/base_volumes_client.py
@@ -62,6 +62,17 @@
self.expected_success(200, resp.status)
return rest_client.ResponseBody(resp, body)
+ def show_pools(self, detail=False):
+ # List all the volumes pools (hosts)
+ url = 'scheduler-stats/get_pools'
+ if detail:
+ url += '?detail=True'
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def show_volume(self, volume_id):
"""Returns the details of a single volume."""
url = "volumes/%s" % str(volume_id)
diff --git a/tempest/test.py b/tempest/test.py
index b32beaa..4bc0ae2 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -350,11 +350,14 @@
@classmethod
def setup_credentials(cls):
- """Allocate credentials and the client managers from them.
+ """Allocate credentials and create the client managers from them.
- A test class that requires network resources must override
- setup_credentials and defined the required resources before super
- is invoked.
+ For every element of credentials param function creates tenant/user,
+ Then it creates client manager for that credential.
+
+ Network related tests must override this function with
+ set_network_resources() method, otherwise it will create
+ network resources(network resources are created in a later step).
"""
for credentials_type in cls.credentials:
# This may raise an exception in case credentials are not available
diff --git a/tempest/tests/fake_auth_provider.py b/tempest/tests/fake_auth_provider.py
index bc68d26..769f6a6 100644
--- a/tempest/tests/fake_auth_provider.py
+++ b/tempest/tests/fake_auth_provider.py
@@ -18,3 +18,9 @@
def auth_request(self, method, url, headers=None, body=None, filters=None):
return url, headers, body
+
+ def get_token(self):
+ return "faketoken"
+
+ def base_url(self, filters, auth_data=None):
+ return "https://example.com"
diff --git a/tempest/tests/services/object_storage/__init__.py b/tempest/tests/services/object_storage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/services/object_storage/__init__.py
diff --git a/tempest/tests/services/object_storage/test_object_client.py b/tempest/tests/services/object_storage/test_object_client.py
new file mode 100644
index 0000000..cd8c8f1
--- /dev/null
+++ b/tempest/tests/services/object_storage/test_object_client.py
@@ -0,0 +1,109 @@
+# Copyright 2016 IBM Corp.
+# 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 mock
+import six
+
+from tempest.lib import exceptions
+from tempest.services.object_storage import object_client
+from tempest.tests import base
+from tempest.tests import fake_auth_provider
+
+
+class TestObjectClient(base.TestCase):
+
+ def setUp(self):
+ super(TestObjectClient, self).setUp()
+ self.fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.url = self.fake_auth.base_url(None)
+ self.object_client = object_client.ObjectClient(self.fake_auth,
+ 'swift', 'region1')
+
+ @mock.patch.object(object_client, 'create_connection')
+ def test_create_object_continue_no_data(self, mock_poc):
+ self._validate_create_object_continue(None, mock_poc)
+
+ @mock.patch.object(object_client, 'create_connection')
+ def test_create_object_continue_with_data(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc)
+
+ @mock.patch.object(object_client, 'create_connection')
+ def test_create_continue_with_no_continue_received(self, mock_poc):
+ self._validate_create_object_continue('hello', mock_poc,
+ initial_status=201)
+
+ def _validate_create_object_continue(self, req_data,
+ mock_poc, initial_status=100):
+
+ expected_hdrs = {
+ 'X-Auth-Token': self.fake_auth.get_token(),
+ 'content-length': 0 if req_data is None else len(req_data),
+ 'Expect': '100-continue'}
+
+ # Setup the Mocks prior to invoking the object creation
+ mock_resp_cls = mock.Mock()
+ mock_resp_cls._read_status.return_value = ("1", initial_status, "OK")
+
+ mock_poc.return_value.response_class.return_value = mock_resp_cls
+
+ # This is the final expected return value
+ mock_poc.return_value.getresponse.return_value.status = 201
+ mock_poc.return_value.getresponse.return_value.reason = 'OK'
+
+ # Call method to PUT object using expect:100-continue
+ cnt = "container1"
+ obj = "object1"
+ path = "/%s/%s" % (cnt, obj)
+
+ # If the expected initial status is not 100, then an exception
+ # should be thrown and the connection closed
+ if initial_status is 100:
+ status, reason = \
+ self.object_client.create_object_continue(cnt, obj, req_data)
+ else:
+ self.assertRaises(exceptions.UnexpectedResponseCode,
+ self.object_client.create_object_continue, cnt,
+ obj, req_data)
+ mock_poc.return_value.close.assert_called_once_with()
+
+ # Verify that putrequest is called 1 time with the appropriate values
+ mock_poc.return_value.putrequest.assert_called_once_with('PUT', path)
+
+ # Verify that headers were written, including "Expect:100-continue"
+ calls = []
+
+ for header, value in six.iteritems(expected_hdrs):
+ calls.append(mock.call(header, value))
+
+ mock_poc.return_value.putheader.assert_has_calls(calls, False)
+ mock_poc.return_value.endheaders.assert_called_once_with()
+
+ # The following steps are only taken if the initial status is 100
+ if initial_status is 100:
+ # Verify that the method returned what it was supposed to
+ self.assertEqual(status, 201)
+
+ # Verify that _safe_read was called once to remove the CRLF
+ # after the 100 response
+ mock_rc = mock_poc.return_value.response_class.return_value
+ mock_rc._safe_read.assert_called_once_with(2)
+
+ # Verify the actual data was written via send
+ mock_poc.return_value.send.assert_called_once_with(req_data)
+
+ # Verify that the getresponse method was called to receive
+ # the final
+ mock_poc.return_value.getresponse.assert_called_once_with()
diff --git a/tempest/tests/test_decorators.py b/tempest/tests/test_decorators.py
index af5fc09..8c5d861 100644
--- a/tempest/tests/test_decorators.py
+++ b/tempest/tests/test_decorators.py
@@ -254,6 +254,13 @@
cfg.CONF.set_default('nova', True, 'service_available')
cfg.CONF.set_default('glance', False, 'service_available')
+ def _assert_skip_message(self, func, skip_msg):
+ try:
+ func()
+ self.fail()
+ except testtools.TestCase.skipException as skip_exc:
+ self.assertEqual(skip_exc.args[0], skip_msg)
+
def _test_skip_unless_config(self, expected_to_skip=True, *decorator_args):
class TestFoo(test.BaseTestCase):
@@ -264,6 +271,9 @@
t = TestFoo('test_bar')
if expected_to_skip:
self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+ if (len(decorator_args) >= 3):
+ # decorator_args[2]: skip message specified
+ self._assert_skip_message(t.test_bar, decorator_args[2])
else:
try:
self.assertEqual(t.test_bar(), 0)
@@ -284,6 +294,9 @@
t = TestFoo('test_bar')
if expected_to_skip:
self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+ if (len(decorator_args) >= 3):
+ # decorator_args[2]: skip message specified
+ self._assert_skip_message(t.test_bar, decorator_args[2])
else:
try:
self.assertEqual(t.test_bar(), 0)
@@ -303,6 +316,10 @@
def test_skip_unless_false_option(self):
self._test_skip_unless_config(True, 'service_available', 'glance')
+ def test_skip_unless_false_option_msg(self):
+ self._test_skip_unless_config(True, 'service_available', 'glance',
+ 'skip message')
+
def test_skip_unless_true_option(self):
self._test_skip_unless_config(False,
'service_available', 'nova')
@@ -318,3 +335,7 @@
def test_skip_if_true_option(self):
self._test_skip_if_config(True, 'service_available', 'nova')
+
+ def test_skip_if_true_option_msg(self):
+ self._test_skip_if_config(True, 'service_available', 'nova',
+ 'skip message')