Merge "Negative tests: Add result check for resources"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index fb11d96..fe4959b 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -398,8 +398,7 @@
#uri_v3=<None>
# Identity API version to be used for authentication for API
-# tests. Planned to extend to tenant isolation, scenario tests
-# and CLI tests. (string value)
+# tests. (string value)
#auth_version=v2
# The identity region name to use. Also used as the other
@@ -753,6 +752,11 @@
# value)
#default_thread_number_per_action=4
+# Prevent the cleaning (tearDownClass()) between each stress
+# test run if an exception occurs during this run. (boolean
+# value)
+#leave_dirty_stack=false
+
[telemetry]
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index da4ccbe..fd069e7 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -161,6 +161,18 @@
return
time.sleep(self.build_interval)
+ @staticmethod
+ def _delete_volume(volumes_client, volume_id):
+ """Deletes the given volume and waits for it to be gone."""
+ try:
+ resp, _ = volumes_client.delete_volume(volume_id)
+ # TODO(mriedem): We should move the wait_for_resource_deletion
+ # into the delete_volume method as a convenience to the caller.
+ volumes_client.wait_for_resource_deletion(volume_id)
+ except exceptions.NotFound:
+ LOG.warn("Unable to delete volume '%s' since it was not found. "
+ "Maybe it was already deleted?" % volume_id)
+
class BaseV2ComputeTest(BaseComputeTest):
@@ -231,14 +243,7 @@
@classmethod
def delete_volume(cls, volume_id):
"""Deletes the given volume and waits for it to be gone."""
- try:
- resp, _ = cls.volumes_extensions_client.delete_volume(volume_id)
- # TODO(mriedem): We should move the wait_for_resource_deletion
- # into the delete_volume method as a convenience to the caller.
- cls.volumes_extensions_client.wait_for_resource_deletion(volume_id)
- except exceptions.NotFound:
- LOG.warn("Unable to delete volume '%s' since it was not found. "
- "Maybe it was already deleted?" % volume_id)
+ cls._delete_volume(cls.volumes_extensions_client, volume_id)
class BaseV2ComputeAdminTest(BaseV2ComputeTest):
@@ -336,6 +341,11 @@
cls.password = server['admin_password']
return server['id']
+ @classmethod
+ def delete_volume(cls, volume_id):
+ """Deletes the given volume and waits for it to be gone."""
+ cls._delete_volume(cls.volumes_client, volume_id)
+
class BaseV3ComputeAdminTest(BaseV3ComputeTest):
"""Base test case class for all Compute Admin API V3 tests."""
diff --git a/tempest/api/compute/servers/test_server_rescue.py b/tempest/api/compute/servers/test_server_rescue.py
index 20c5d7f..0bf604c 100644
--- a/tempest/api/compute/servers/test_server_rescue.py
+++ b/tempest/api/compute/servers/test_server_rescue.py
@@ -24,6 +24,7 @@
@classmethod
def setUpClass(cls):
+ cls.set_network_resources(network=True, subnet=True, router=True)
super(ServerRescueTestJSON, cls).setUpClass()
cls.device = 'vdf'
@@ -41,20 +42,10 @@
cls.sg_id = cls.sg['id']
# Create a volume and wait for it to become ready for attach
- resp, cls.volume_to_attach = \
- cls.volumes_extensions_client.create_volume(1,
- display_name=
- 'test_attach')
+ resp, cls.volume = cls.volumes_extensions_client.create_volume(
+ 1, display_name=data_utils.rand_name(cls.__name__ + '_volume'))
cls.volumes_extensions_client.wait_for_volume_status(
- cls.volume_to_attach['id'], 'available')
-
- # Create a volume and wait for it to become ready for attach
- resp, cls.volume_to_detach = \
- cls.volumes_extensions_client.create_volume(1,
- display_name=
- 'test_detach')
- cls.volumes_extensions_client.wait_for_volume_status(
- cls.volume_to_detach['id'], 'available')
+ cls.volume['id'], 'available')
# Server for positive tests
resp, server = cls.create_test_server(wait_until='BUILD')
@@ -78,9 +69,7 @@
def tearDownClass(cls):
# Deleting the floating IP which is created in this method
cls.floating_ips_client.delete_floating_ip(cls.floating_ip_id)
- client = cls.volumes_extensions_client
- client.delete_volume(str(cls.volume_to_attach['id']).strip())
- client.delete_volume(str(cls.volume_to_detach['id']).strip())
+ cls.delete_volume(cls.volume['id'])
resp, cls.sg = cls.security_groups_client.delete_security_group(
cls.sg_id)
super(ServerRescueTestJSON, cls).tearDownClass()
@@ -93,9 +82,6 @@
self.volumes_extensions_client.wait_for_volume_status(volume_id,
'available')
- def _delete(self, volume_id):
- self.volumes_extensions_client.delete_volume(volume_id)
-
def _unrescue(self, server_id):
resp, body = self.servers_client.unrescue_server(server_id)
self.assertEqual(202, resp.status)
@@ -159,32 +145,31 @@
self.assertRaises(exceptions.Conflict,
self.servers_client.attach_volume,
self.server_id,
- self.volume_to_attach['id'],
+ self.volume['id'],
device='/dev/%s' % self.device)
@attr(type=['negative', 'gate'])
def test_rescued_vm_detach_volume(self):
# Attach the volume to the server
self.servers_client.attach_volume(self.server_id,
- self.volume_to_detach['id'],
+ self.volume['id'],
device='/dev/%s' % self.device)
self.volumes_extensions_client.wait_for_volume_status(
- self.volume_to_detach['id'], 'in-use')
+ self.volume['id'], 'in-use')
# Rescue the server
self.servers_client.rescue_server(self.server_id,
adminPass=self.password)
self.servers_client.wait_for_server_status(self.server_id, 'RESCUE')
# addCleanup is a LIFO queue
- self.addCleanup(self._detach, self.server_id,
- self.volume_to_detach['id'])
+ self.addCleanup(self._detach, self.server_id, self.volume['id'])
self.addCleanup(self._unrescue, self.server_id)
# Detach the volume from the server expecting failure
self.assertRaises(exceptions.Conflict,
self.servers_client.detach_volume,
self.server_id,
- self.volume_to_detach['id'])
+ self.volume['id'])
@attr(type='gate')
def test_rescued_vm_associate_dissociate_floating_ip(self):
diff --git a/tempest/api/compute/v3/servers/test_server_metadata_negative.py b/tempest/api/compute/v3/servers/test_server_metadata_negative.py
index 2c413db..ce6c340 100644
--- a/tempest/api/compute/v3/servers/test_server_metadata_negative.py
+++ b/tempest/api/compute/v3/servers/test_server_metadata_negative.py
@@ -34,6 +34,7 @@
cls.server_id = server['id']
+ @test.skip_because(bug="1273948")
@test.attr(type=['gate', 'negative'])
def test_server_create_metadata_key_too_long(self):
# Attempt to start a server with a meta-data key that is > 255
@@ -43,7 +44,7 @@
for sz in [256, 257, 511, 1023]:
key = "k" * sz
meta = {key: 'data1'}
- self.assertRaises(exceptions.OverLimit,
+ self.assertRaises(exceptions.BadRequest,
self.create_test_server,
meta=meta)
diff --git a/tempest/api/compute/v3/servers/test_server_rescue.py b/tempest/api/compute/v3/servers/test_server_rescue.py
index f8be1c1..fa7def0 100644
--- a/tempest/api/compute/v3/servers/test_server_rescue.py
+++ b/tempest/api/compute/v3/servers/test_server_rescue.py
@@ -14,6 +14,7 @@
# under the License.
from tempest.api.compute import base
+from tempest.common.utils import data_utils
from tempest import exceptions
from tempest.test import attr
@@ -27,20 +28,10 @@
cls.device = 'vdf'
# Create a volume and wait for it to become ready for attach
- resp, cls.volume_to_attach = \
- cls.volumes_client.create_volume(1,
- display_name=
- 'test_attach')
+ resp, cls.volume = cls.volumes_client.create_volume(
+ 1, display_name=data_utils.rand_name(cls.__name__ + '_volume'))
cls.volumes_client.wait_for_volume_status(
- cls.volume_to_attach['id'], 'available')
-
- # Create a volume and wait for it to become ready for attach
- resp, cls.volume_to_detach = \
- cls.volumes_client.create_volume(1,
- display_name=
- 'test_detach')
- cls.volumes_client.wait_for_volume_status(
- cls.volume_to_detach['id'], 'available')
+ cls.volume['id'], 'available')
# Server for positive tests
resp, server = cls.create_test_server(wait_until='BUILD')
@@ -62,9 +53,7 @@
@classmethod
def tearDownClass(cls):
- client = cls.volumes_client
- client.delete_volume(str(cls.volume_to_attach['id']).strip())
- client.delete_volume(str(cls.volume_to_detach['id']).strip())
+ cls.delete_volume(cls.volume['id'])
super(ServerRescueV3Test, cls).tearDownClass()
def tearDown(self):
@@ -75,9 +64,6 @@
self.volumes_client.wait_for_volume_status(volume_id,
'available')
- def _delete(self, volume_id):
- self.volumes_client.delete_volume(volume_id)
-
def _unrescue(self, server_id):
resp, body = self.servers_client.unrescue_server(server_id)
self.assertEqual(202, resp.status)
@@ -141,29 +127,27 @@
self.assertRaises(exceptions.Conflict,
self.servers_client.attach_volume,
self.server_id,
- self.volume_to_attach['id'],
+ self.volume['id'],
device='/dev/%s' % self.device)
@attr(type=['negative', 'gate'])
def test_rescued_vm_detach_volume(self):
# Attach the volume to the server
self.servers_client.attach_volume(self.server_id,
- self.volume_to_detach['id'],
+ self.volume['id'],
device='/dev/%s' % self.device)
- self.volumes_client.wait_for_volume_status(
- self.volume_to_detach['id'], 'in-use')
+ self.volumes_client.wait_for_volume_status(self.volume['id'], 'in-use')
# Rescue the server
self.servers_client.rescue_server(self.server_id,
admin_password=self.password)
self.servers_client.wait_for_server_status(self.server_id, 'RESCUE')
# addCleanup is a LIFO queue
- self.addCleanup(self._detach, self.server_id,
- self.volume_to_detach['id'])
+ self.addCleanup(self._detach, self.server_id, self.volume['id'])
self.addCleanup(self._unrescue, self.server_id)
# Detach the volume from the server expecting failure
self.assertRaises(exceptions.Conflict,
self.servers_client.detach_volume,
self.server_id,
- self.volume_to_detach['id'])
+ self.volume['id'])
diff --git a/tempest/api/compute/v3/servers/test_servers_negative.py b/tempest/api/compute/v3/servers/test_servers_negative.py
index c153699..12e0ad8 100644
--- a/tempest/api/compute/v3/servers/test_servers_negative.py
+++ b/tempest/api/compute/v3/servers/test_servers_negative.py
@@ -191,12 +191,13 @@
self.create_test_server,
key_name=key_name)
+ @test.skip_because(bug="1273948")
@test.attr(type=['negative', 'gate'])
def test_create_server_metadata_exceeds_length_limit(self):
# Pass really long metadata while creating a server
metadata = {'a': 'b' * 260}
- self.assertRaises(exceptions.OverLimit,
+ self.assertRaises(exceptions.BadRequest,
self.create_test_server,
meta=metadata)
diff --git a/tempest/api/data_processing/test_plugins.py b/tempest/api/data_processing/test_plugins.py
new file mode 100644
index 0000000..3b941d8
--- /dev/null
+++ b/tempest/api/data_processing/test_plugins.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2013 Mirantis Inc.
+#
+# 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.data_processing import base as dp_base
+from tempest.test import attr
+
+
+class PluginsTest(dp_base.BaseDataProcessingTest):
+ def _list_all_plugin_names(self):
+ """Returns all enabled plugin names.
+
+ It ensures response status and main plugins availability.
+ """
+ resp, plugins = self.client.list_plugins()
+
+ self.assertEqual(200, resp.status)
+
+ plugins_names = list([plugin['name'] for plugin in plugins])
+ self.assertIn('vanilla', plugins_names)
+ self.assertIn('hdp', plugins_names)
+
+ return plugins_names
+
+ @attr(type='smoke')
+ def test_plugin_list(self):
+ self._list_all_plugin_names()
+
+ @attr(type='smoke')
+ def test_plugin_get(self):
+ for plugin_name in self._list_all_plugin_names():
+ resp, plugin = self.client.get_plugin(plugin_name)
+
+ self.assertEqual(200, resp.status)
+ self.assertEqual(plugin_name, plugin['name'])
+
+ for plugin_version in plugin['versions']:
+ resp, detailed_plugin = self.client.get_plugin(plugin_name,
+ plugin_version)
+
+ self.assertEqual(200, resp.status)
+ self.assertEqual(plugin_name, detailed_plugin['name'])
+
+ # check that required image tags contains name and version
+ image_tags = detailed_plugin['required_image_tags']
+ self.assertIn(plugin_name, image_tags)
+ self.assertIn(plugin_version, image_tags)
diff --git a/tempest/api/identity/admin/test_users_negative.py b/tempest/api/identity/admin/test_users_negative.py
index e9e7818..060f24a 100644
--- a/tempest/api/identity/admin/test_users_negative.py
+++ b/tempest/api/identity/admin/test_users_negative.py
@@ -207,7 +207,7 @@
@attr(type=['negative', 'gate'])
def test_get_users_request_without_token(self):
# Request to get list of users without a valid token should fail
- token = self.client.auth_provider.auth_data[0]
+ token = self.client.auth_provider.get_token()
self.client.delete_token(token)
self.assertRaises(exceptions.Unauthorized, self.client.get_users)
self.client.auth_provider.clear_auth()
diff --git a/tempest/api/image/base.py b/tempest/api/image/base.py
index 37b848c..e439238 100644
--- a/tempest/api/image/base.py
+++ b/tempest/api/image/base.py
@@ -106,7 +106,7 @@
cls.os_alt = clients.AltManager()
identity_client = cls._get_identity_admin_client()
cls.alt_tenant_id = identity_client.get_tenant_by_name(
- cls.os_alt.tenant_name)['id']
+ cls.os_alt.credentials['tenant_name'])['id']
cls.alt_img_cli = cls.os_alt.image_client
@@ -147,7 +147,7 @@
cls.alt_tenant_id = cls.isolated_creds.get_alt_tenant()['id']
else:
cls.os_alt = clients.AltManager()
- alt_tenant_name = cls.os_alt.tenant_name
+ alt_tenant_name = cls.os_alt.credentials['tenant_name']
identity_client = cls._get_identity_admin_client()
cls.alt_tenant_id = identity_client.get_tenant_by_name(
alt_tenant_name)['id']
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 8c62c05..d8b79ca 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -71,7 +71,7 @@
resp, body = self.create_image(name='New Http Image',
container_format='bare',
disk_format='raw', is_public=True,
- copy_from=CONF.images.http_image)
+ copy_from=CONF.image.http_image)
self.assertIn('id', body)
image_id = body.get('id')
self.assertEqual('New Http Image', body.get('name'))
diff --git a/tempest/api/network/admin/test_l3_agent_scheduler.py b/tempest/api/network/admin/test_l3_agent_scheduler.py
index bfb7b48..7c02787 100644
--- a/tempest/api/network/admin/test_l3_agent_scheduler.py
+++ b/tempest/api/network/admin/test_l3_agent_scheduler.py
@@ -17,7 +17,7 @@
from tempest import test
-class L3AgentSchedulerJSON(base.BaseAdminNetworkTest):
+class L3AgentSchedulerTestJSON(base.BaseAdminNetworkTest):
_interface = 'json'
"""
@@ -33,7 +33,7 @@
@classmethod
def setUpClass(cls):
- super(L3AgentSchedulerJSON, cls).setUpClass()
+ super(L3AgentSchedulerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('l3_agent_scheduler', 'network'):
msg = "L3 Agent Scheduler Extension not enabled."
raise cls.skipException(msg)
@@ -61,5 +61,5 @@
self.assertEqual(204, resp.status)
-class L3AgentSchedulerXML(L3AgentSchedulerJSON):
+class L3AgentSchedulerTestXML(L3AgentSchedulerTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index 1c2c4b0..b129786 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -49,6 +49,8 @@
neutron as True
"""
+ force_tenant_isolation = False
+
@classmethod
def setUpClass(cls):
# Create no network resources for these test.
@@ -57,6 +59,10 @@
os = clients.Manager(interface=cls._interface)
if not CONF.service_available.neutron:
raise cls.skipException("Neutron support is required")
+
+ os = cls.get_client_manager()
+
+ cls.network_cfg = CONF.network
cls.client = os.network_client
cls.networks = []
cls.subnets = []
@@ -110,6 +116,7 @@
# Clean up networks
for network in cls.networks:
cls.client.delete_network(network['id'])
+ cls.clear_isolated_creds()
super(BaseNetworkTest, cls).tearDownClass()
@classmethod
@@ -269,5 +276,14 @@
msg = ("Missing Administrative Network API credentials "
"in configuration.")
raise cls.skipException(msg)
- cls.admin_manager = clients.AdminManager(interface=cls._interface)
- cls.admin_client = cls.admin_manager.network_client
+ if (CONF.compute.allow_tenant_isolation or
+ cls.force_tenant_isolation is True):
+ creds = cls.isolated_creds.get_admin_creds()
+ admin_username, admin_tenant_name, admin_password = creds
+ cls.os_adm = clients.Manager(username=admin_username,
+ password=admin_password,
+ tenant_name=admin_tenant_name,
+ interface=cls._interface)
+ else:
+ cls.os_adm = clients.ComputeAdminManager(interface=cls._interface)
+ cls.admin_client = cls.os_adm.network_client
diff --git a/tempest/api/network/common.py b/tempest/api/network/common.py
index 0ce1769..d68ff1a 100644
--- a/tempest/api/network/common.py
+++ b/tempest/api/network/common.py
@@ -126,3 +126,21 @@
def delete(self):
self.client.delete_security_group_rule(self.id)
+
+
+class DeletablePool(DeletableResource):
+
+ def delete(self):
+ self.client.delete_pool(self.id)
+
+
+class DeletableMember(DeletableResource):
+
+ def delete(self):
+ self.client.delete_member(self.id)
+
+
+class DeletableVip(DeletableResource):
+
+ def delete(self):
+ self.client.delete_vip(self.id)
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index 65eebf2..d5f2b5b 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -18,7 +18,7 @@
from tempest import test
-class LoadBalancerJSON(base.BaseNetworkTest):
+class LoadBalancerTestJSON(base.BaseNetworkTest):
_interface = 'json'
"""
@@ -39,7 +39,7 @@
@classmethod
def setUpClass(cls):
- super(LoadBalancerJSON, cls).setUpClass()
+ super(LoadBalancerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas', 'network'):
msg = "lbaas extension not enabled."
raise cls.skipException(msg)
@@ -210,5 +210,5 @@
self.assertEqual('204', resp['status'])
-class LoadBalancerXML(LoadBalancerJSON):
+class LoadBalancerTestXML(LoadBalancerTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index 3aa765c..aee2a44 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -239,7 +239,7 @@
_interface = 'xml'
-class BulkNetworkOpsJSON(base.BaseNetworkTest):
+class BulkNetworkOpsTestJSON(base.BaseNetworkTest):
_interface = 'json'
"""
@@ -263,7 +263,7 @@
@classmethod
def setUpClass(cls):
- super(BulkNetworkOpsJSON, cls).setUpClass()
+ super(BulkNetworkOpsTestJSON, cls).setUpClass()
cls.network1 = cls.create_network()
cls.network2 = cls.create_network()
@@ -390,5 +390,5 @@
self.assertIn(n['id'], ports_list)
-class BulkNetworkOpsXML(BulkNetworkOpsJSON):
+class BulkNetworkOpsTestXML(BulkNetworkOpsTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_vpnaas_extensions.py b/tempest/api/network/test_vpnaas_extensions.py
index 64b8a41..78bc80a 100644
--- a/tempest/api/network/test_vpnaas_extensions.py
+++ b/tempest/api/network/test_vpnaas_extensions.py
@@ -38,10 +38,10 @@
@classmethod
def setUpClass(cls):
- super(VPNaaSJSON, cls).setUpClass()
if not test.is_extension_enabled('vpnaas', 'network'):
msg = "vpnaas extension not enabled."
raise cls.skipException(msg)
+ super(VPNaaSJSON, cls).setUpClass()
cls.network = cls.create_network()
cls.subnet = cls.create_subnet(cls.network)
cls.router = cls.create_router(
diff --git a/tempest/api/volume/admin/test_multi_backend.py b/tempest/api/volume/admin/test_multi_backend.py
index cb1a6cb..b0c878b 100644
--- a/tempest/api/volume/admin/test_multi_backend.py
+++ b/tempest/api/volume/admin/test_multi_backend.py
@@ -14,8 +14,6 @@
from tempest.common.utils import data_utils
from tempest import config
from tempest.openstack.common import log as logging
-from tempest.services.volume.json.admin import volume_types_client
-from tempest.services.volume.json import volumes_client
from tempest.test import attr
CONF = config.CONF
@@ -36,20 +34,7 @@
cls.backend1_name = CONF.volume.backend1_name
cls.backend2_name = CONF.volume.backend2_name
- adm_user = CONF.identity.admin_username
- adm_pass = CONF.identity.admin_password
- adm_tenant = CONF.identity.admin_tenant_name
- auth_url = CONF.identity.uri
-
- cls.volume_client = volumes_client.VolumesClientJSON(adm_user,
- adm_pass,
- auth_url,
- adm_tenant)
- cls.type_client = volume_types_client.VolumeTypesClientJSON(adm_user,
- adm_pass,
- auth_url,
- adm_tenant)
-
+ cls.volume_client = cls.os_adm.volumes_client
cls.volume_type_id_list = []
cls.volume_id_list = []
try:
@@ -57,7 +42,7 @@
type1_name = data_utils.rand_name('Type-')
vol1_name = data_utils.rand_name('Volume-')
extra_specs1 = {"volume_backend_name": cls.backend1_name}
- resp, cls.type1 = cls.type_client.create_volume_type(
+ resp, cls.type1 = cls.client.create_volume_type(
type1_name, extra_specs=extra_specs1)
cls.volume_type_id_list.append(cls.type1['id'])
@@ -72,7 +57,7 @@
type2_name = data_utils.rand_name('Type-')
vol2_name = data_utils.rand_name('Volume-')
extra_specs2 = {"volume_backend_name": cls.backend2_name}
- resp, cls.type2 = cls.type_client.create_volume_type(
+ resp, cls.type2 = cls.client.create_volume_type(
type2_name, extra_specs=extra_specs2)
cls.volume_type_id_list.append(cls.type2['id'])
@@ -97,7 +82,7 @@
# volume types deletion
volume_type_id_list = getattr(cls, 'volume_type_id_list', [])
for volume_type_id in volume_type_id_list:
- cls.type_client.delete_volume_type(volume_type_id)
+ cls.client.delete_volume_type(volume_type_id)
super(VolumeMultiBackendTest, cls).tearDownClass()
diff --git a/tempest/api/volume/test_volume_transfers.py b/tempest/api/volume/test_volume_transfers.py
index fc4f07d..cf4e052 100644
--- a/tempest/api/volume/test_volume_transfers.py
+++ b/tempest/api/volume/test_volume_transfers.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from testtools import matchers
+
from tempest.api.volume import base
from tempest import clients
from tempest import config
@@ -87,7 +89,7 @@
# or equal to 1
resp, body = self.client.list_volume_transfers()
self.assertEqual(200, resp.status)
- self.assertGreaterEqual(len(body), 1)
+ self.assertThat(len(body), matchers.GreaterThan(0))
# Accept a volume transfer by alt_tenant
resp, body = self.alt_client.accept_volume_transfer(transfer_id,
@@ -107,10 +109,14 @@
self.client.wait_for_volume_status(volume['id'],
'awaiting-transfer')
- # List all volume transfers, there's only one in this test
+ # List all volume transfers (looking for the one we created)
resp, body = self.client.list_volume_transfers()
self.assertEqual(200, resp.status)
- self.assertEqual(volume['id'], body[0]['volume_id'])
+ for transfer in body:
+ if volume['id'] == transfer['volume_id']:
+ break
+ else:
+ self.fail('Transfer not found for volume %s' % volume['id'])
# Delete a volume transfer
resp, body = self.client.delete_volume_transfer(transfer_id)
diff --git a/tempest/api/volume/v2/__init__.py b/tempest/api/volume/v2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api/volume/v2/__init__.py
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
new file mode 100644
index 0000000..049544d
--- /dev/null
+++ b/tempest/api/volume/v2/test_volumes_list.py
@@ -0,0 +1,228 @@
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 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 operator
+
+from tempest.api.volume import base
+from tempest.common.utils import data_utils
+from tempest.openstack.common import log as logging
+from tempest.test import attr
+from testtools.matchers import ContainsAll
+
+LOG = logging.getLogger(__name__)
+
+VOLUME_FIELDS = ('id', 'display_name')
+
+
+class VolumesListTest(base.BaseVolumeV1Test):
+
+ """
+ This test creates a number of 1G volumes. To run successfully,
+ ensure that the backing file for the volume group that Nova uses
+ has space for at least 3 1G volumes!
+ If you are running a Devstack environment, ensure that the
+ VOLUME_BACKING_FILE_SIZE is at least 4G in your localrc
+ """
+
+ _interface = 'json'
+
+ def assertVolumesIn(self, fetched_list, expected_list, fields=None):
+ if fields:
+ expected_list = map(operator.itemgetter(*fields), expected_list)
+ fetched_list = map(operator.itemgetter(*fields), fetched_list)
+
+ missing_vols = [v for v in expected_list if v not in fetched_list]
+ if len(missing_vols) == 0:
+ return
+
+ def str_vol(vol):
+ return "%s:%s" % (vol['id'], vol['display_name'])
+
+ raw_msg = "Could not find volumes %s in expected list %s; fetched %s"
+ self.fail(raw_msg % ([str_vol(v) for v in missing_vols],
+ [str_vol(v) for v in expected_list],
+ [str_vol(v) for v in fetched_list]))
+
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesListTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ # Create 3 test volumes
+ cls.volume_list = []
+ cls.volume_id_list = []
+ cls.metadata = {'Type': 'work'}
+ for i in range(3):
+ try:
+ volume = cls.create_volume(metadata=cls.metadata)
+
+ resp, volume = cls.client.get_volume(volume['id'])
+ cls.volume_list.append(volume)
+ cls.volume_id_list.append(volume['id'])
+ except Exception:
+ LOG.exception('Failed to create volume. %d volumes were '
+ 'created' % len(cls.volume_id_list))
+ if cls.volume_list:
+ # We could not create all the volumes, though we were able
+ # to create *some* of the volumes. This is typically
+ # because the backing file size of the volume group is
+ # too small.
+ for volid in cls.volume_id_list:
+ cls.client.delete_volume(volid)
+ cls.client.wait_for_resource_deletion(volid)
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ # Delete the created volumes
+ for volid in cls.volume_id_list:
+ resp, _ = cls.client.delete_volume(volid)
+ cls.client.wait_for_resource_deletion(volid)
+ super(VolumesListTest, cls).tearDownClass()
+
+ def _list_by_param_value_and_assert(self, params, with_detail=False):
+ """
+ Perform list or list_details action with given params
+ and validates result.
+ """
+ if with_detail:
+ resp, fetched_vol_list = \
+ self.client.list_volumes_with_detail(params=params)
+ else:
+ resp, fetched_vol_list = self.client.list_volumes(params=params)
+
+ self.assertEqual(200, resp.status)
+ # Validating params of fetched volumes
+ for volume in fetched_vol_list:
+ for key in params:
+ msg = "Failed to list volumes %s by %s" % \
+ ('details' if with_detail else '', key)
+ if key == 'metadata':
+ self.assertThat(volume[key].items(),
+ ContainsAll(params[key].items()),
+ msg)
+ else:
+ self.assertEqual(params[key], volume[key], msg)
+
+ @attr(type='smoke')
+ def test_volume_list(self):
+ # Get a list of Volumes
+ # Fetch all volumes
+ resp, fetched_list = self.client.list_volumes()
+ self.assertEqual(200, resp.status)
+ self.assertVolumesIn(fetched_list, self.volume_list,
+ fields=VOLUME_FIELDS)
+
+ @attr(type='gate')
+ def test_volume_list_with_details(self):
+ # Get a list of Volumes with details
+ # Fetch all Volumes
+ resp, fetched_list = self.client.list_volumes_with_detail()
+ self.assertEqual(200, resp.status)
+ self.assertVolumesIn(fetched_list, self.volume_list)
+
+ @attr(type='gate')
+ def test_volume_list_by_name(self):
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ params = {'display_name': volume['display_name']}
+ resp, fetched_vol = self.client.list_volumes(params)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(1, len(fetched_vol), str(fetched_vol))
+ self.assertEqual(fetched_vol[0]['display_name'],
+ volume['display_name'])
+
+ @attr(type='gate')
+ def test_volume_list_details_by_name(self):
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ params = {'display_name': volume['display_name']}
+ resp, fetched_vol = self.client.list_volumes_with_detail(params)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(1, len(fetched_vol), str(fetched_vol))
+ self.assertEqual(fetched_vol[0]['display_name'],
+ volume['display_name'])
+
+ @attr(type='gate')
+ def test_volumes_list_by_status(self):
+ params = {'status': 'available'}
+ resp, fetched_list = self.client.list_volumes(params)
+ self.assertEqual(200, resp.status)
+ for volume in fetched_list:
+ self.assertEqual('available', volume['status'])
+ self.assertVolumesIn(fetched_list, self.volume_list,
+ fields=VOLUME_FIELDS)
+
+ @attr(type='gate')
+ def test_volumes_list_details_by_status(self):
+ params = {'status': 'available'}
+ resp, fetched_list = self.client.list_volumes_with_detail(params)
+ self.assertEqual(200, resp.status)
+ for volume in fetched_list:
+ self.assertEqual('available', volume['status'])
+ self.assertVolumesIn(fetched_list, self.volume_list)
+
+ @attr(type='gate')
+ def test_volumes_list_by_availability_zone(self):
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ zone = volume['availability_zone']
+ params = {'availability_zone': zone}
+ resp, fetched_list = self.client.list_volumes(params)
+ self.assertEqual(200, resp.status)
+ for volume in fetched_list:
+ self.assertEqual(zone, volume['availability_zone'])
+ self.assertVolumesIn(fetched_list, self.volume_list,
+ fields=VOLUME_FIELDS)
+
+ @attr(type='gate')
+ def test_volumes_list_details_by_availability_zone(self):
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ zone = volume['availability_zone']
+ params = {'availability_zone': zone}
+ resp, fetched_list = self.client.list_volumes_with_detail(params)
+ self.assertEqual(200, resp.status)
+ for volume in fetched_list:
+ self.assertEqual(zone, volume['availability_zone'])
+ self.assertVolumesIn(fetched_list, self.volume_list)
+
+ @attr(type='gate')
+ def test_volume_list_with_param_metadata(self):
+ # Test to list volumes when metadata param is given
+ params = {'metadata': self.metadata}
+ self._list_by_param_value_and_assert(params)
+
+ @attr(type='gate')
+ def test_volume_list_with_detail_param_metadata(self):
+ # Test to list volumes details when metadata param is given
+ params = {'metadata': self.metadata}
+ self._list_by_param_value_and_assert(params, with_detail=True)
+
+ @attr(type='gate')
+ def test_volume_list_param_display_name_and_status(self):
+ # Test to list volume when display name and status param is given
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ params = {'display_name': volume['display_name'],
+ 'status': 'available'}
+ self._list_by_param_value_and_assert(params)
+
+ @attr(type='gate')
+ def test_volume_list_with_detail_param_display_name_and_status(self):
+ # Test to list volume when name and status param is given
+ volume = self.volume_list[data_utils.rand_int_id(0, 2)]
+ params = {'display_name': volume['display_name'],
+ 'status': 'available'}
+ self._list_by_param_value_and_assert(params, with_detail=True)
+
+
+class VolumeListTestXML(VolumesListTest):
+ _interface = 'xml'
diff --git a/tempest/auth.py b/tempest/auth.py
index 8d826cf..582cfdd 100644
--- a/tempest/auth.py
+++ b/tempest/auth.py
@@ -68,10 +68,10 @@
"""
Decorate request with authentication data
"""
- raise NotImplemented
+ raise NotImplementedError
def _get_auth(self):
- raise NotImplemented
+ raise NotImplementedError
@classmethod
def check_credentials(cls, credentials):
@@ -98,7 +98,7 @@
self.cache = None
def is_expired(self, auth_data):
- raise NotImplemented
+ raise NotImplementedError
def auth_request(self, method, url, headers=None, body=None, filters=None):
"""
@@ -110,9 +110,6 @@
:param filters: select a base URL out of the catalog
:returns a Tuple (url, headers, body)
"""
- LOG.debug("Auth request m:{m}, u:{u}, h:{h}, b:{b}, f:{f}".format(
- m=method, u=url, h=headers, b=body, f=filters
- ))
orig_req = dict(url=url, headers=headers, body=body)
auth_url, auth_headers, auth_body = self._decorate_request(
@@ -127,7 +124,6 @@
auth_data=self.alt_auth_data)
alt_auth_req = dict(url=alt_url, headers=alt_headers,
body=alt_body)
- self._log_auth_request(alt_auth_req, 'ALTERNATE')
auth_req[self.alt_part] = alt_auth_req[self.alt_part]
else:
@@ -137,21 +133,8 @@
# Next auth request will be normal, unless otherwise requested
self.reset_alt_auth_data()
- self._log_auth_request(auth_req, 'Authorized Request:')
-
return auth_req['url'], auth_req['headers'], auth_req['body']
- def _log_auth_request(self, auth_req, tag):
- url = auth_req.get('url', None)
- headers = copy.deepcopy(auth_req.get('headers', None))
- body = auth_req.get('body', None)
- if headers is not None:
- if 'X-Auth-Token' in headers.keys():
- headers['X-Auth-Token'] = '<Token Omitted>'
- LOG.debug("[{tag}]: u: {url}, h: {headers}, b: {body}".format(
- tag=tag, url=url, headers=headers, body=body
- ))
-
def reset_alt_auth_data(self):
"""
Configure auth provider to provide valid authentication data
@@ -176,7 +159,7 @@
"""
Extracts the base_url based on provided filters
"""
- raise NotImplemented
+ raise NotImplementedError
class KeystoneAuthProvider(AuthProvider):
@@ -208,10 +191,10 @@
return _url, _headers, body
def _auth_client(self):
- raise NotImplemented
+ raise NotImplementedError
def _auth_params(self):
- raise NotImplemented
+ raise NotImplementedError
def _get_auth(self):
# Bypasses the cache
@@ -223,7 +206,7 @@
token, auth_data = auth_func(**auth_params)
return token, auth_data
else:
- raise NotImplemented
+ raise NotImplementedError
def get_token(self):
return self.auth_data[0]
@@ -250,7 +233,7 @@
else:
return xml_id.TokenClientXML()
else:
- raise NotImplemented
+ raise NotImplementedError
def _auth_params(self):
if self.client_type == 'tempest':
@@ -260,7 +243,7 @@
tenant=self.credentials.get('tenant_name', None),
auth_data=True)
else:
- raise NotImplemented
+ raise NotImplementedError
def base_url(self, filters, auth_data=None):
"""
@@ -299,7 +282,7 @@
path = "/" + filters['api_version']
noversion_path = "/".join(parts.path.split("/")[2:])
if noversion_path != "":
- path += noversion_path
+ path += "/" + noversion_path
_base_url = _base_url.replace(parts.path, path)
if filters.get('skip_path', None) is not None:
_base_url = _base_url.replace(parts.path, "/")
@@ -334,7 +317,7 @@
else:
return xml_v3id.V3TokenClientXML()
else:
- raise NotImplemented
+ raise NotImplementedError
def _auth_params(self):
if self.client_type == 'tempest':
@@ -345,7 +328,7 @@
domain=self.credentials['domain_name'],
auth_data=True)
else:
- raise NotImplemented
+ raise NotImplementedError
def base_url(self, filters, auth_data=None):
"""
diff --git a/tempest/clients.py b/tempest/clients.py
index fd46656..9c1a0f1 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -445,6 +445,6 @@
base = super(OrchestrationManager, self)
base.__init__(CONF.identity.admin_username,
CONF.identity.admin_password,
- CONF.identity.tenant_name,
+ CONF.identity.admin_tenant_name,
interface=interface,
service=service)
diff --git a/tempest/common/isolated_creds.py b/tempest/common/isolated_creds.py
index 146fac9..ac8b14f 100644
--- a/tempest/common/isolated_creds.py
+++ b/tempest/common/isolated_creds.py
@@ -36,7 +36,6 @@
self.isolated_net_resources = {}
self.ports = []
self.name = name
- self.config = CONF
self.tempest_client = tempest_client
self.interface = interface
self.password = password
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 8a85602..212d41d 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -38,7 +38,21 @@
class RestClient(object):
+
TYPE = "json"
+
+ # This is used by _parse_resp method
+ # Redefine it for purposes of your xml service client
+ # List should contain top-xml_tag-names of data, which is like list/array
+ # For example, in keystone it is users, roles, tenants and services
+ # All of it has children with same tag-names
+ list_tags = []
+
+ # This is used by _parse_resp method too
+ # Used for selection of dict-like xmls,
+ # like metadata for Vms in nova, and volumes in cinder
+ dict_tags = ["metadata", ]
+
LOG = logging.getLogger(__name__)
def __init__(self, auth_provider):
@@ -49,6 +63,9 @@
# The version of the API this client implements
self.api_version = None
self._skip_path = False
+ # NOTE(vponomaryov): self.headers is deprecated now.
+ # should be removed after excluding it from all use places.
+ # Insted of this should be used 'get_headers' method
self.headers = {'Content-Type': 'application/%s' % self.TYPE,
'Accept': 'application/%s' % self.TYPE}
self.build_interval = CONF.compute.build_interval
@@ -65,6 +82,19 @@
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
+ def _get_type(self):
+ return self.TYPE
+
+ def get_headers(self, accept_type=None, send_type=None):
+ # This method should be used instead of
+ # deprecated 'self.headers'
+ if accept_type is None:
+ accept_type = self._get_type()
+ if send_type is None:
+ send_type = self._get_type()
+ return {'Content-Type': 'application/%s' % send_type,
+ 'Accept': 'application/%s' % accept_type}
+
def __str__(self):
STRING_LIMIT = 80
str_format = ("config:%s, service:%s, base_url:%s, "
@@ -74,7 +104,7 @@
self.filters, self.build_interval,
self.build_timeout,
str(self.token)[0:STRING_LIMIT],
- str(self.headers)[0:STRING_LIMIT])
+ str(self.get_headers())[0:STRING_LIMIT])
def _get_region(self, service):
"""
@@ -108,6 +138,10 @@
return self.auth_provider.base_url(filters=self.filters)
@property
+ def token(self):
+ return self.auth_provider.get_token()
+
+ @property
def filters(self):
_filters = dict(
service=self.service,
@@ -146,7 +180,7 @@
details = pattern.format(read_code, expected_code)
raise exceptions.InvalidHttpSuccessCode(details)
- def post(self, url, body, headers):
+ def post(self, url, body, headers=None):
return self.request('POST', url, headers, body)
def get(self, url, headers=None):
@@ -155,10 +189,10 @@
def delete(self, url, headers=None, body=None):
return self.request('DELETE', url, headers, body)
- def patch(self, url, body, headers):
+ def patch(self, url, body, headers=None):
return self.request('PATCH', url, headers, body)
- def put(self, url, body, headers):
+ def put(self, url, body, headers=None):
return self.request('PUT', url, headers, body)
def head(self, url, headers=None):
@@ -214,7 +248,48 @@
hashlib.md5(str_body).hexdigest())
def _parse_resp(self, body):
- return json.loads(body)
+ if self._get_type() is "json":
+ body = json.loads(body)
+
+ # We assume, that if the first value of the deserialized body's
+ # item set is a dict or a list, that we just return the first value
+ # of deserialized body.
+ # Essentially "cutting out" the first placeholder element in a body
+ # that looks like this:
+ #
+ # {
+ # "users": [
+ # ...
+ # ]
+ # }
+ try:
+ # Ensure there are not more than one top-level keys
+ if len(body.keys()) > 1:
+ return body
+ # Just return the "wrapped" element
+ first_key, first_item = body.items()[0]
+ if isinstance(first_item, (dict, list)):
+ return first_item
+ except (ValueError, IndexError):
+ pass
+ return body
+ elif self._get_type() is "xml":
+ element = etree.fromstring(body)
+ if any(s in element.tag for s in self.dict_tags):
+ # Parse dictionary-like xmls (metadata, etc)
+ dictionary = {}
+ for el in element.getchildren():
+ dictionary[u"%s" % el.get("key")] = u"%s" % el.text
+ return dictionary
+ if any(s in element.tag for s in self.list_tags):
+ # Parse list-like xmls (users, roles, etc)
+ array = []
+ for child in element.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ # Parse one-item-like xmls (user, role, etc)
+ return xml_to_json(element)
def response_checker(self, method, url, headers, body, resp, resp_body):
if (resp.status in set((204, 205, 304)) or resp.status < 200 or
@@ -244,8 +319,7 @@
if not resp_body and resp.status >= 400:
self.LOG.warning("status >= 400 response with empty body")
- def _request(self, method, url,
- headers=None, body=None):
+ def _request(self, method, url, headers=None, body=None):
"""A simple HTTP request interface."""
# Authenticate the request with the auth provider
req_url, req_headers, req_body = self.auth_provider.auth_request(
@@ -261,12 +335,13 @@
return resp, resp_body
- def request(self, method, url,
- headers=None, body=None):
+ def request(self, method, url, headers=None, body=None):
retry = 0
if headers is None:
- headers = {}
+ # NOTE(vponomaryov): if some client do not need headers,
+ # it should explicitly pass empty dict
+ headers = self.get_headers()
resp, resp_body = self._request(method, url,
headers=headers, body=body)
@@ -386,10 +461,13 @@
if (not isinstance(resp_body, collections.Mapping) or
'retry-after' not in resp):
return True
- over_limit = resp_body.get('overLimit', None)
- if not over_limit:
- return True
- return 'exceed' in over_limit.get('message', 'blabla')
+ if self._get_type() is "json":
+ over_limit = resp_body.get('overLimit', None)
+ if not over_limit:
+ return True
+ return 'exceed' in over_limit.get('message', 'blabla')
+ elif self._get_type() is "xml":
+ return 'exceed' in resp_body.get('message', 'blabla')
def wait_for_resource_deletion(self, id):
"""Waits for a resource to be deleted."""
@@ -411,6 +489,11 @@
class RestClientXML(RestClient):
+
+ # NOTE(vponomaryov): This is deprecated class
+ # and should be removed after excluding it
+ # from all service clients
+
TYPE = "xml"
def _parse_resp(self, body):
@@ -436,11 +519,11 @@
if method == "GET":
resp, body = self.get(url)
elif method == "POST":
- resp, body = self.post(url, body, self.headers)
+ resp, body = self.post(url, body)
elif method == "PUT":
- resp, body = self.put(url, body, self.headers)
+ resp, body = self.put(url, body)
elif method == "PATCH":
- resp, body = self.patch(url, body, self.headers)
+ resp, body = self.patch(url, body)
elif method == "HEAD":
resp, body = self.head(url)
elif method == "DELETE":
diff --git a/tempest/common/ssh.py b/tempest/common/ssh.py
index 0ed9b82..c772ce9 100644
--- a/tempest/common/ssh.py
+++ b/tempest/common/ssh.py
@@ -49,7 +49,7 @@
self.channel_timeout = float(channel_timeout)
self.buf_size = 1024
- def _get_ssh_connection(self, sleep=1.5, backoff=1.01):
+ def _get_ssh_connection(self, sleep=1.5, backoff=1):
"""Returns an ssh connection to the specified host."""
bsleep = sleep
ssh = paramiko.SSHClient()
@@ -76,19 +76,21 @@
self.username, self.host)
return ssh
except (socket.error,
- paramiko.SSHException):
- attempts += 1
- time.sleep(bsleep)
- bsleep *= backoff
- if not self._is_timed_out(_start_time):
- continue
- else:
+ paramiko.SSHException) as e:
+ if self._is_timed_out(_start_time):
LOG.exception("Failed to establish authenticated ssh"
" connection to %s@%s after %d attempts",
self.username, self.host, attempts)
raise exceptions.SSHTimeout(host=self.host,
user=self.username,
password=self.password)
+ bsleep += backoff
+ attempts += 1
+ LOG.warning("Failed to establish authenticated ssh"
+ " connection to %s@%s (%s). Number attempts: %s."
+ " Retry after %d seconds.",
+ self.username, self.host, e, attempts, bsleep)
+ time.sleep(bsleep)
def _is_timed_out(self, start_time):
return (time.time() - self.timeout) > start_time
diff --git a/tempest/config.py b/tempest/config.py
index cb186ca..d24ab34 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -46,8 +46,7 @@
cfg.StrOpt('auth_version',
default='v2',
help="Identity API version to be used for authentication "
- "for API tests. Planned to extend to tenant isolation, "
- "scenario tests and CLI tests."),
+ "for API tests."),
cfg.StrOpt('region',
default='RegionOne',
help="The identity region name to use. Also used as the other "
@@ -587,7 +586,12 @@
help='time (in seconds) between log file error checks.'),
cfg.IntOpt('default_thread_number_per_action',
default=4,
- help='The number of threads created while stress test.')
+ help='The number of threads created while stress test.'),
+ cfg.BoolOpt('leave_dirty_stack',
+ default=False,
+ help='Prevent the cleaning (tearDownClass()) between'
+ ' each stress test run if an exception occurs'
+ ' during this run.')
]
@@ -781,7 +785,7 @@
self.compute_feature_enabled = cfg.CONF['compute-feature-enabled']
self.identity = cfg.CONF.identity
self.identity_feature_enabled = cfg.CONF['identity-feature-enabled']
- self.images = cfg.CONF.image
+ self.image = cfg.CONF.image
self.image_feature_enabled = cfg.CONF['image-feature-enabled']
self.network = cfg.CONF.network
self.network_feature_enabled = cfg.CONF['network-feature-enabled']
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 59a3aeb..0fc304a 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -22,7 +22,7 @@
import cinderclient.client
import glanceclient
import heatclient.client
-import keystoneclient.apiclient.exceptions
+import keystoneclient.exceptions
import keystoneclient.v2_0.client
import netaddr
from neutronclient.common import exceptions as exc
@@ -112,7 +112,7 @@
region = CONF.identity.region
endpoint = self.identity_client.service_catalog.url_for(
attr='region', filter_value=region,
- service_type='image', endpoint_type='publicURL')
+ service_type=CONF.image.catalog_type, endpoint_type='publicURL')
dscv = CONF.identity.disable_ssl_certificate_validation
return glanceclient.Client('1', endpoint=endpoint, token=token,
insecure=dscv)
@@ -146,7 +146,7 @@
keystone_admin.roles.add_user_role(self.identity_client.user_id,
member_role.id,
self.identity_client.tenant_id)
- except keystoneclient.apiclient.exceptions.Conflict:
+ except keystoneclient.exceptions.Conflict:
pass
return swiftclient.Connection(auth_url, username, password,
@@ -167,11 +167,12 @@
keystone = self._get_identity_client(username, password, tenant_name)
region = CONF.identity.region
token = keystone.auth_token
+ service_type = CONF.orchestration.catalog_type
try:
endpoint = keystone.service_catalog.url_for(
attr='region',
filter_value=region,
- service_type='orchestration',
+ service_type=service_type,
endpoint_type='publicURL')
except keystoneclient.exceptions.EndpointNotFound:
return None
@@ -670,13 +671,17 @@
"Unable to determine which port to target.")
return ports[0]['id']
- def _create_floating_ip(self, server, external_network_id):
- port_id = self._get_server_port_id(server)
+ def _create_floating_ip(self, thing, external_network_id,
+ port_filters=None):
+ if port_filters is None:
+ port_id = self._get_server_port_id(thing)
+ else:
+ port_id = port_filters
body = dict(
floatingip=dict(
floating_network_id=external_network_id,
port_id=port_id,
- tenant_id=server.tenant_id,
+ tenant_id=thing.tenant_id,
)
)
result = self.network_client.create_floatingip(body=body)
@@ -713,6 +718,58 @@
return tempest.test.call_until_true(
ping, CONF.compute.ping_timeout, 1)
+ def _create_pool(self, lb_method, protocol, subnet_id):
+ """Wrapper utility that returns a test pool."""
+ name = data_utils.rand_name('pool-')
+ body = {
+ "pool": {
+ "protocol": protocol,
+ "name": name,
+ "subnet_id": subnet_id,
+ "lb_method": lb_method
+ }
+ }
+ resp = self.network_client.create_pool(body=body)
+ pool = net_common.DeletablePool(client=self.network_client,
+ **resp['pool'])
+ self.assertEqual(pool['name'], name)
+ self.set_resource(name, pool)
+ return pool
+
+ def _create_member(self, address, protocol_port, pool_id):
+ """Wrapper utility that returns a test member."""
+ body = {
+ "member": {
+ "protocol_port": protocol_port,
+ "pool_id": pool_id,
+ "address": address
+ }
+ }
+ resp = self.network_client.create_member(body)
+ member = net_common.DeletableMember(client=self.network_client,
+ **resp['member'])
+ self.set_resource(data_utils.rand_name('member-'), member)
+ return member
+
+ def _create_vip(self, protocol, protocol_port, subnet_id, pool_id):
+ """Wrapper utility that returns a test vip."""
+ name = data_utils.rand_name('vip-')
+ body = {
+ "vip": {
+ "protocol": protocol,
+ "name": name,
+ "subnet_id": subnet_id,
+ "pool_id": pool_id,
+ "protocol_port": protocol_port
+ }
+ }
+ resp = self.network_client.create_vip(body)
+ vip = net_common.DeletableVip(client=self.network_client,
+ **resp['vip'])
+ self.assertEqual(vip['name'], name)
+ self.set_resource(name, vip)
+ return vip
+
def _check_vm_connectivity(self, ip_address,
username=None,
private_key=None,
diff --git a/tempest/scenario/test_load_balancer_basic.py b/tempest/scenario/test_load_balancer_basic.py
new file mode 100644
index 0000000..68f6e62
--- /dev/null
+++ b/tempest/scenario/test_load_balancer_basic.py
@@ -0,0 +1,240 @@
+# Copyright 2014 Mirantis.inc
+# 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 time
+import urllib
+
+from tempest.api.network import common as net_common
+from tempest.common import ssh
+from tempest.common.utils import data_utils
+from tempest import config
+from tempest import exceptions
+from tempest.scenario import manager
+from tempest import test
+
+config = config.CONF
+
+
+class TestLoadBalancerBasic(manager.NetworkScenarioTest):
+
+ """
+ This test checks basic load balancing.
+
+ The following is the scenario outline:
+ 1. Create an instance
+ 2. SSH to the instance and start two servers
+ 3. Create a load balancer with two members and with ROUND_ROBIN algorithm
+ associate the VIP with a floating ip
+ 4. Send 10 requests to the floating ip and check that they are shared
+ between the two servers and that both of them get equal portions
+ of the requests
+ """
+
+ @classmethod
+ def check_preconditions(cls):
+ super(TestLoadBalancerBasic, cls).check_preconditions()
+ cfg = config.network
+ if not test.is_extension_enabled('lbaas', 'network'):
+ msg = 'LBaaS Extension is not enabled'
+ cls.enabled = False
+ raise cls.skipException(msg)
+ if not (cfg.tenant_networks_reachable or cfg.public_network_id):
+ msg = ('Either tenant_networks_reachable must be "true", or '
+ 'public_network_id must be defined.')
+ cls.enabled = False
+ raise cls.skipException(msg)
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestLoadBalancerBasic, cls).setUpClass()
+ cls.check_preconditions()
+ cls.security_groups = {}
+ cls.networks = []
+ cls.subnets = []
+ cls.servers_keypairs = {}
+ cls.pools = []
+ cls.members = []
+ cls.vips = []
+ cls.floating_ips = {}
+ cls.port1 = 80
+ cls.port2 = 88
+
+ def _create_security_groups(self):
+ self.security_groups[self.tenant_id] =\
+ self._create_security_group_neutron(tenant_id=self.tenant_id)
+
+ def _create_server(self):
+ tenant_id = self.tenant_id
+ name = data_utils.rand_name("smoke_server-")
+ keypair = self.create_keypair(name='keypair-%s' % name)
+ security_groups = [self.security_groups[tenant_id].name]
+ nets = self.network_client.list_networks()
+ for net in nets['networks']:
+ if net['tenant_id'] == self.tenant_id:
+ self.networks.append(net)
+ create_kwargs = {
+ 'nics': [
+ {'net-id': net['id']},
+ ],
+ 'key_name': keypair.name,
+ 'security_groups': security_groups,
+ }
+ server = self.create_server(name=name,
+ create_kwargs=create_kwargs)
+ self.servers_keypairs[server] = keypair
+ break
+ self.assertTrue(self.servers_keypairs)
+
+ def _start_servers(self):
+ """
+ 1. SSH to the instance
+ 2. Start two servers listening on ports 80 and 88 respectively
+ """
+ for server in self.servers_keypairs.keys():
+ ssh_login = config.compute.image_ssh_user
+ private_key = self.servers_keypairs[server].private_key
+ network_name = self.networks[0]['name']
+
+ ip_address = server.networks[network_name][0]
+ ssh_client = ssh.Client(ip_address, ssh_login,
+ pkey=private_key,
+ timeout=100)
+ start_server = "while true; do echo -e 'HTTP/1.0 200 OK\r\n\r\n" \
+ "%(server)s' | sudo nc -l -p %(port)s ; done &"
+ cmd = start_server % {'server': 'server1',
+ 'port': self.port1}
+ ssh_client.exec_command(cmd)
+ cmd = start_server % {'server': 'server2',
+ 'port': self.port2}
+ ssh_client.exec_command(cmd)
+
+ def _check_connection(self, check_ip):
+ def try_connect(ip):
+ try:
+ urllib.urlopen("http://{0}/".format(ip))
+ return True
+ except IOError:
+ return False
+ timeout = config.compute.ping_timeout
+ timer = 0
+ while not try_connect(check_ip):
+ time.sleep(1)
+ timer += 1
+ if timer >= timeout:
+ message = "Timed out trying to connect to %s" % check_ip
+ raise exceptions.TimeoutException(message)
+
+ def _create_pool(self):
+ """Create a pool with ROUND_ROBIN algorithm."""
+ subnets = self.network_client.list_subnets()
+ for subnet in subnets['subnets']:
+ if subnet['tenant_id'] == self.tenant_id:
+ self.subnets.append(subnet)
+ pool = super(TestLoadBalancerBasic, self)._create_pool(
+ 'ROUND_ROBIN',
+ 'HTTP',
+ subnet['id'])
+ self.pools.append(pool)
+ break
+ self.assertTrue(self.pools)
+
+ def _create_members(self, network_name, server_ids):
+ """
+ Create two members.
+
+ In case there is only one server, create both members with the same ip
+ but with different ports to listen on.
+ """
+ servers = self.compute_client.servers.list()
+ for server in servers:
+ if server.id in server_ids:
+ ip = server.networks[network_name][0]
+ pool_id = self.pools[0]['id']
+ if len(set(server_ids)) == 1 or len(servers) == 1:
+ member1 = self._create_member(ip, self.port1, pool_id)
+ member2 = self._create_member(ip, self.port2, pool_id)
+ self.members.extend([member1, member2])
+ else:
+ member = self._create_member(ip, self.port1, pool_id)
+ self.members.append(member)
+ self.assertTrue(self.members)
+
+ def _assign_floating_ip_to_vip(self, vip):
+ public_network_id = config.network.public_network_id
+ port_id = vip['port_id']
+ floating_ip = self._create_floating_ip(vip,
+ public_network_id,
+ port_filters=port_id)
+ self.floating_ips.setdefault(vip['id'], [])
+ self.floating_ips[vip['id']].append(floating_ip)
+
+ def _create_load_balancer(self):
+ self._create_pool()
+ self._create_members(self.networks[0]['name'],
+ [self.servers_keypairs.keys()[0].id])
+ subnet_id = self.subnets[0]['id']
+ pool_id = self.pools[0]['id']
+ vip = super(TestLoadBalancerBasic, self)._create_vip('HTTP', 80,
+ subnet_id,
+ pool_id)
+ self.vips.append(vip)
+ self._status_timeout(NeutronRetriever(self.network_client,
+ self.network_client.vip_path,
+ net_common.DeletableVip),
+ self.vips[0]['id'],
+ expected_status='ACTIVE')
+ self._assign_floating_ip_to_vip(self.vips[0])
+
+ def _check_load_balancing(self):
+ """
+ 1. Send 10 requests on the floating ip associated with the VIP
+ 2. Check that the requests are shared between
+ the two servers and that both of them get equal portions
+ of the requests
+ """
+
+ vip = self.vips[0]
+ floating_ip_vip = self.floating_ips[
+ vip['id']][0]['floating_ip_address']
+ self._check_connection(floating_ip_vip)
+ resp = []
+ for count in range(10):
+ resp.append(
+ urllib.urlopen(
+ "http://{0}/".format(floating_ip_vip)).read())
+ self.assertEqual(set(["server1\n", "server2\n"]), set(resp))
+ self.assertEqual(5, resp.count("server1\n"))
+ self.assertEqual(5, resp.count("server2\n"))
+
+ @test.skip_because(bug="1277381")
+ @test.attr(type='smoke')
+ @test.services('compute', 'network')
+ def test_load_balancer_basic(self):
+ self._create_security_groups()
+ self._create_server()
+ self._start_servers()
+ self._create_load_balancer()
+ self._check_load_balancing()
+
+
+class NeutronRetriever(object):
+ def __init__(self, network_client, path, resource):
+ self.network_client = network_client
+ self.path = path
+ self.resource = resource
+
+ def get(self, thing_id):
+ obj = self.network_client.get(self.path % thing_id)
+ return self.resource(client=self.network_client, **obj.values()[0])
diff --git a/tempest/services/data_processing/v1_1/client.py b/tempest/services/data_processing/v1_1/client.py
index db21201..e96b44b 100644
--- a/tempest/services/data_processing/v1_1/client.py
+++ b/tempest/services/data_processing/v1_1/client.py
@@ -77,3 +77,17 @@
uri = "node-group-templates/%s" % tmpl_id
return self.delete(uri)
+
+ def list_plugins(self):
+ """List all enabled plugins."""
+
+ uri = 'plugins'
+ return self._request_and_parse(self.get, uri, 'plugins')
+
+ def get_plugin(self, plugin_name, plugin_version=None):
+ """Returns the details of a single plugin."""
+
+ uri = "plugins/%s" % plugin_name
+ if plugin_version:
+ uri += '/%s' % plugin_version
+ return self._request_and_parse(self.get, uri, 'plugin')
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index c018215..349a9e9 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -12,20 +12,23 @@
import json
-from tempest.common.rest_client import RestClient
+from tempest.common import rest_client
from tempest import config
from tempest import exceptions
CONF = config.CONF
-class IdentityClientJSON(RestClient):
+class IdentityClientJSON(rest_client.RestClient):
def __init__(self, auth_provider):
super(IdentityClientJSON, self).__init__(auth_provider)
self.service = CONF.identity.catalog_type
self.endpoint_url = 'adminURL'
+ # Needed for xml service client
+ self.list_tags = ["roles", "tenants", "users", "services"]
+
def has_admin_extensions(self):
"""
Returns True if the KSADM Admin Extensions are supported
@@ -43,9 +46,8 @@
'name': name,
}
post_body = json.dumps({'role': post_body})
- resp, body = self.post('OS-KSADM/roles', post_body, self.headers)
- body = json.loads(body)
- return resp, body['role']
+ resp, body = self.post('OS-KSADM/roles', post_body)
+ return resp, self._parse_resp(body)
def create_tenant(self, name, **kwargs):
"""
@@ -60,30 +62,24 @@
'enabled': kwargs.get('enabled', True),
}
post_body = json.dumps({'tenant': post_body})
- resp, body = self.post('tenants', post_body, self.headers)
- body = json.loads(body)
- return resp, body['tenant']
+ resp, body = self.post('tenants', post_body)
+ return resp, self._parse_resp(body)
def delete_role(self, role_id):
"""Delete a role."""
- resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id))
- return resp, body
+ return self.delete('OS-KSADM/roles/%s' % str(role_id))
def list_user_roles(self, tenant_id, user_id):
"""Returns a list of roles assigned to a user for a tenant."""
url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
resp, body = self.get(url)
- body = json.loads(body)
- return resp, body['roles']
+ return resp, self._parse_resp(body)
def assign_user_role(self, tenant_id, user_id, role_id):
"""Add roles to a user on a tenant."""
- post_body = json.dumps({})
resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
- (tenant_id, user_id, role_id), post_body,
- self.headers)
- body = json.loads(body)
- return resp, body['role']
+ (tenant_id, user_id, role_id), "")
+ return resp, self._parse_resp(body)
def remove_user_role(self, tenant_id, user_id, role_id):
"""Removes a role assignment for a user on a tenant."""
@@ -92,20 +88,17 @@
def delete_tenant(self, tenant_id):
"""Delete a tenant."""
- resp, body = self.delete('tenants/%s' % str(tenant_id))
- return resp, body
+ return self.delete('tenants/%s' % str(tenant_id))
def get_tenant(self, tenant_id):
"""Get tenant details."""
resp, body = self.get('tenants/%s' % str(tenant_id))
- body = json.loads(body)
- return resp, body['tenant']
+ return resp, self._parse_resp(body)
def list_roles(self):
"""Returns roles."""
resp, body = self.get('OS-KSADM/roles')
- body = json.loads(body)
- return resp, body['roles']
+ return resp, self._parse_resp(body)
def list_tenants(self):
"""Returns tenants."""
@@ -133,10 +126,8 @@
'enabled': en,
}
post_body = json.dumps({'tenant': post_body})
- resp, body = self.post('tenants/%s' % tenant_id, post_body,
- self.headers)
- body = json.loads(body)
- return resp, body['tenant']
+ resp, body = self.post('tenants/%s' % tenant_id, post_body)
+ return resp, self._parse_resp(body)
def create_user(self, name, password, tenant_id, email, **kwargs):
"""Create a user."""
@@ -149,34 +140,28 @@
if kwargs.get('enabled') is not None:
post_body['enabled'] = kwargs.get('enabled')
post_body = json.dumps({'user': post_body})
- resp, body = self.post('users', post_body, self.headers)
- body = json.loads(body)
- return resp, body['user']
+ resp, body = self.post('users', post_body)
+ return resp, self._parse_resp(body)
def update_user(self, user_id, **kwargs):
"""Updates a user."""
put_body = json.dumps({'user': kwargs})
- resp, body = self.put('users/%s' % user_id, put_body,
- self.headers)
- body = json.loads(body)
- return resp, body['user']
+ resp, body = self.put('users/%s' % user_id, put_body)
+ return resp, self._parse_resp(body)
def get_user(self, user_id):
"""GET a user."""
resp, body = self.get("users/%s" % user_id)
- body = json.loads(body)
- return resp, body['user']
+ return resp, self._parse_resp(body)
def delete_user(self, user_id):
"""Delete a user."""
- resp, body = self.delete("users/%s" % user_id)
- return resp, body
+ return self.delete("users/%s" % user_id)
def get_users(self):
"""Get the list of users."""
resp, body = self.get("users")
- body = json.loads(body)
- return resp, body['users']
+ return resp, self._parse_resp(body)
def enable_disable_user(self, user_id, enabled):
"""Enables or disables a user."""
@@ -184,21 +169,17 @@
'enabled': enabled
}
put_body = json.dumps({'user': put_body})
- resp, body = self.put('users/%s/enabled' % user_id,
- put_body, self.headers)
- body = json.loads(body)
- return resp, body
+ resp, body = self.put('users/%s/enabled' % user_id, put_body)
+ return resp, self._parse_resp(body)
def delete_token(self, token_id):
"""Delete a token."""
- resp, body = self.delete("tokens/%s" % token_id)
- return resp, body
+ return self.delete("tokens/%s" % token_id)
def list_users_for_tenant(self, tenant_id):
"""List users for a Tenant."""
resp, body = self.get('/tenants/%s/users' % tenant_id)
- body = json.loads(body)
- return resp, body['users']
+ return resp, self._parse_resp(body)
def get_user_by_username(self, tenant_id, username):
resp, users = self.list_users_for_tenant(tenant_id)
@@ -215,22 +196,19 @@
'description': kwargs.get('description')
}
post_body = json.dumps({'OS-KSADM:service': post_body})
- resp, body = self.post('/OS-KSADM/services', post_body, self.headers)
- body = json.loads(body)
- return resp, body['OS-KSADM:service']
+ resp, body = self.post('/OS-KSADM/services', post_body)
+ return resp, self._parse_resp(body)
def get_service(self, service_id):
"""Get Service."""
url = '/OS-KSADM/services/%s' % service_id
resp, body = self.get(url)
- body = json.loads(body)
- return resp, body['OS-KSADM:service']
+ return resp, self._parse_resp(body)
def list_services(self):
"""List Service - Returns Services."""
resp, body = self.get('/OS-KSADM/services/')
- body = json.loads(body)
- return resp, body['OS-KSADM:services']
+ return resp, self._parse_resp(body)
def delete_service(self, service_id):
"""Delete Service."""
@@ -238,7 +216,7 @@
return self.delete(url)
-class TokenClientJSON(RestClient):
+class TokenClientJSON(IdentityClientJSON):
def __init__(self):
super(TokenClientJSON, self).__init__(None)
@@ -261,15 +239,17 @@
}
}
body = json.dumps(creds)
- resp, body = self.post(self.auth_url, headers=self.headers, body=body)
+ resp, body = self.post(self.auth_url, body=body)
return resp, body['access']
def request(self, method, url, headers=None, body=None):
"""A simple HTTP request interface."""
if headers is None:
- headers = {}
-
+ # Always accept 'json', for TokenClientXML too.
+ # Because XML response is not easily
+ # converted to the corresponding JSON one
+ headers = self.get_headers(accept_type="json")
self._log_request(method, url, headers, body)
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
@@ -282,7 +262,9 @@
raise exceptions.IdentityError(
'Unexpected status code {0}'.format(resp.status))
- return resp, json.loads(resp_body)
+ if isinstance(resp_body, str):
+ resp_body = json.loads(resp_body)
+ return resp, resp_body
def get_token(self, user, password, tenant, auth_data=False):
"""
diff --git a/tempest/services/identity/xml/identity_client.py b/tempest/services/identity/xml/identity_client.py
index 7c36680..81846da 100644
--- a/tempest/services/identity/xml/identity_client.py
+++ b/tempest/services/identity/xml/identity_client.py
@@ -12,58 +12,24 @@
# 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 lxml import etree
-
-from tempest.common.rest_client import RestClientXML
from tempest import config
-from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml import common as xml
+from tempest.services.identity.json import identity_client
CONF = config.CONF
XMLNS = "http://docs.openstack.org/identity/api/v2.0"
-class IdentityClientXML(RestClientXML):
-
- def __init__(self, auth_provider):
- super(IdentityClientXML, self).__init__(auth_provider)
- self.service = CONF.identity.catalog_type
- self.endpoint_url = 'adminURL'
-
- def _parse_array(self, node):
- array = []
- for child in node.getchildren():
- array.append(xml_to_json(child))
- return array
-
- def _parse_body(self, body):
- data = xml_to_json(body)
- return data
-
- def has_admin_extensions(self):
- """
- Returns True if the KSADM Admin Extensions are supported
- False otherwise
- """
- if hasattr(self, '_has_admin_extensions'):
- return self._has_admin_extensions
- resp, body = self.list_roles()
- self._has_admin_extensions = ('status' in resp and resp.status != 503)
- return self._has_admin_extensions
+class IdentityClientXML(identity_client.IdentityClientJSON):
+ TYPE = "xml"
def create_role(self, name):
"""Create a role."""
- create_role = Element("role", xmlns=XMLNS, name=name)
- resp, body = self.post('OS-KSADM/roles', str(Document(create_role)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
+ create_role = xml.Element("role", xmlns=XMLNS, name=name)
+ resp, body = self.post('OS-KSADM/roles',
+ str(xml.Document(create_role)))
+ return resp, self._parse_resp(body)
def create_tenant(self, name, **kwargs):
"""
@@ -73,70 +39,18 @@
enabled <true|false>: Initial tenant status (default is true)
"""
en = kwargs.get('enabled', 'true')
- create_tenant = Element("tenant",
- xmlns=XMLNS,
- name=name,
- description=kwargs.get('description', ''),
- enabled=str(en).lower())
- resp, body = self.post('tenants', str(Document(create_tenant)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def delete_role(self, role_id):
- """Delete a role."""
- resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id),
- self.headers)
- return resp, body
-
- def list_user_roles(self, tenant_id, user_id):
- """Returns a list of roles assigned to a user for a tenant."""
- url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
- resp, body = self.get(url, self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
-
- def assign_user_role(self, tenant_id, user_id, role_id):
- """Add roles to a user on a tenant."""
- resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
- (tenant_id, user_id, role_id), '', self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def remove_user_role(self, tenant_id, user_id, role_id):
- """Removes a role assignment for a user on a tenant."""
- return self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s' %
- (tenant_id, user_id, role_id), self.headers)
-
- def delete_tenant(self, tenant_id):
- """Delete a tenant."""
- resp, body = self.delete('tenants/%s' % str(tenant_id), self.headers)
- return resp, body
-
- def get_tenant(self, tenant_id):
- """Get tenant details."""
- resp, body = self.get('tenants/%s' % str(tenant_id), self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def list_roles(self):
- """Returns roles."""
- resp, body = self.get('OS-KSADM/roles', self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
+ create_tenant = xml.Element("tenant",
+ xmlns=XMLNS,
+ name=name,
+ description=kwargs.get('description', ''),
+ enabled=str(en).lower())
+ resp, body = self.post('tenants', str(xml.Document(create_tenant)))
+ return resp, self._parse_resp(body)
def list_tenants(self):
"""Returns tenants."""
- resp, body = self.get('tenants', self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
-
- def get_tenant_by_name(self, tenant_name):
- resp, tenants = self.list_tenants()
- for tenant in tenants:
- if tenant['name'] == tenant_name:
- return tenant
- raise exceptions.NotFound('No such tenant')
+ resp, body = self.get('tenants')
+ return resp, self._parse_resp(body)
def update_tenant(self, tenant_id, **kwargs):
"""Updates a tenant."""
@@ -144,173 +58,69 @@
name = kwargs.get('name', body['name'])
desc = kwargs.get('description', body['description'])
en = kwargs.get('enabled', body['enabled'])
- update_tenant = Element("tenant",
- xmlns=XMLNS,
- id=tenant_id,
- name=name,
- description=desc,
- enabled=str(en).lower())
+ update_tenant = xml.Element("tenant",
+ xmlns=XMLNS,
+ id=tenant_id,
+ name=name,
+ description=desc,
+ enabled=str(en).lower())
resp, body = self.post('tenants/%s' % tenant_id,
- str(Document(update_tenant)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
+ str(xml.Document(update_tenant)))
+ return resp, self._parse_resp(body)
def create_user(self, name, password, tenant_id, email, **kwargs):
"""Create a user."""
- create_user = Element("user",
- xmlns=XMLNS,
- name=name,
- password=password,
- tenantId=tenant_id,
- email=email)
+ create_user = xml.Element("user",
+ xmlns=XMLNS,
+ name=name,
+ password=password,
+ tenantId=tenant_id,
+ email=email)
if 'enabled' in kwargs:
create_user.add_attr('enabled', str(kwargs['enabled']).lower())
- resp, body = self.post('users', str(Document(create_user)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
+ resp, body = self.post('users', str(xml.Document(create_user)))
+ return resp, self._parse_resp(body)
def update_user(self, user_id, **kwargs):
"""Updates a user."""
if 'enabled' in kwargs:
kwargs['enabled'] = str(kwargs['enabled']).lower()
- update_user = Element("user", xmlns=XMLNS, **kwargs)
+ update_user = xml.Element("user", xmlns=XMLNS, **kwargs)
resp, body = self.put('users/%s' % user_id,
- str(Document(update_user)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def get_user(self, user_id):
- """GET a user."""
- resp, body = self.get("users/%s" % user_id, self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def delete_user(self, user_id):
- """Delete a user."""
- resp, body = self.delete("users/%s" % user_id, self.headers)
- return resp, body
-
- def get_users(self):
- """Get the list of users."""
- resp, body = self.get("users", self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
+ str(xml.Document(update_user)))
+ return resp, self._parse_resp(body)
def enable_disable_user(self, user_id, enabled):
"""Enables or disables a user."""
- enable_user = Element("user", enabled=str(enabled).lower())
+ enable_user = xml.Element("user", enabled=str(enabled).lower())
resp, body = self.put('users/%s/enabled' % user_id,
- str(Document(enable_user)), self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
+ str(xml.Document(enable_user)), self.headers)
+ return resp, self._parse_resp(body)
- def delete_token(self, token_id):
- """Delete a token."""
- resp, body = self.delete("tokens/%s" % token_id, self.headers)
- return resp, body
-
- def list_users_for_tenant(self, tenant_id):
- """List users for a Tenant."""
- resp, body = self.get('/tenants/%s/users' % tenant_id, self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
-
- def get_user_by_username(self, tenant_id, username):
- resp, users = self.list_users_for_tenant(tenant_id)
- for user in users:
- if user['name'] == username:
- return user
- raise exceptions.NotFound('No such user')
-
- def create_service(self, name, type, **kwargs):
+ def create_service(self, name, service_type, **kwargs):
"""Create a service."""
OS_KSADM = "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0"
- create_service = Element("service",
- xmlns=OS_KSADM,
- name=name,
- type=type,
- description=kwargs.get('description'))
+ create_service = xml.Element("service",
+ xmlns=OS_KSADM,
+ name=name,
+ type=service_type,
+ description=kwargs.get('description'))
resp, body = self.post('OS-KSADM/services',
- str(Document(create_service)),
- self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def list_services(self):
- """Returns services."""
- resp, body = self.get('OS-KSADM/services', self.headers)
- body = self._parse_array(etree.fromstring(body))
- return resp, body
-
- def get_service(self, service_id):
- """Get Service."""
- url = '/OS-KSADM/services/%s' % service_id
- resp, body = self.get(url, self.headers)
- body = self._parse_body(etree.fromstring(body))
- return resp, body
-
- def delete_service(self, service_id):
- """Delete Service."""
- url = '/OS-KSADM/services/%s' % service_id
- return self.delete(url, self.headers)
+ str(xml.Document(create_service)))
+ return resp, self._parse_resp(body)
-class TokenClientXML(RestClientXML):
-
- def __init__(self):
- super(TokenClientXML, self).__init__(None)
- auth_url = CONF.identity.uri
-
- # Normalize URI to ensure /tokens is in it.
- if 'tokens' not in auth_url:
- auth_url = auth_url.rstrip('/') + '/tokens'
-
- self.auth_url = auth_url
+class TokenClientXML(identity_client.TokenClientJSON):
+ TYPE = "xml"
def auth(self, user, password, tenant):
- passwordCreds = Element("passwordCredentials",
- username=user,
- password=password)
- auth = Element("auth", tenantName=tenant)
+ passwordCreds = xml.Element("passwordCredentials",
+ username=user,
+ password=password)
+ auth = xml.Element("auth", tenantName=tenant)
auth.append(passwordCreds)
- resp, body = self.post(self.auth_url, headers=self.headers,
- body=str(Document(auth)))
+ resp, body = self.post(self.auth_url, body=str(xml.Document(auth)))
return resp, body['access']
-
- def request(self, method, url, headers=None, body=None):
- """A simple HTTP request interface."""
- if headers is None:
- headers = {}
- # Send XML, accept JSON. XML response is not easily
- # converted to the corresponding JSON one
- headers['Accept'] = 'application/json'
- self._log_request(method, url, headers, body)
- resp, resp_body = self.http_obj.request(url, method,
- headers=headers, body=body)
- self._log_response(resp, resp_body)
-
- if resp.status in [401, 403]:
- resp_body = json.loads(resp_body)
- raise exceptions.Unauthorized(resp_body['error']['message'])
- elif resp.status not in [200, 201]:
- raise exceptions.IdentityError(
- 'Unexpected status code {0}'.format(resp.status))
-
- return resp, json.loads(resp_body)
-
- def get_token(self, user, password, tenant, auth_data=False):
- """
- Returns (token id, token data) for supplied credentials
- """
- resp, body = self.auth(user, password, tenant)
-
- if auth_data:
- return body['token']['id'], body
- else:
- return body['token']['id']
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/image_client.py
index 17271cc..bc9db38 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/image_client.py
@@ -35,7 +35,7 @@
def __init__(self, auth_provider):
super(ImageClientJSON, self).__init__(auth_provider)
- self.service = CONF.images.catalog_type
+ self.service = CONF.image.catalog_type
self._http = None
def _image_meta_from_headers(self, headers):
diff --git a/tempest/services/image/v2/json/image_client.py b/tempest/services/image/v2/json/image_client.py
index 38aef2d..b825519 100644
--- a/tempest/services/image/v2/json/image_client.py
+++ b/tempest/services/image/v2/json/image_client.py
@@ -30,7 +30,7 @@
def __init__(self, auth_provider):
super(ImageClientV2JSON, self).__init__(auth_provider)
- self.service = CONF.images.catalog_type
+ self.service = CONF.image.catalog_type
self._http = None
def _get_http(self):
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
index 924d9a8..efac5f5 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/services/object_storage/account_client.py
@@ -30,10 +30,6 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- @property
- def token(self):
- return self.auth_provider.auth_data[0]
-
def create_account(self, data=None,
params=None,
metadata={},
@@ -62,7 +58,7 @@
url += 'bulk-delete&'
url = '?%s%s' % (url, urllib.urlencode(params))
- resp, body = self.delete(url, headers=None, body=data)
+ resp, body = self.delete(url, headers={}, body=data)
return resp, body
def list_account_metadata(self):
diff --git a/tempest/services/object_storage/container_client.py b/tempest/services/object_storage/container_client.py
index 63a6460..f224407 100644
--- a/tempest/services/object_storage/container_client.py
+++ b/tempest/services/object_storage/container_client.py
@@ -32,10 +32,6 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- @property
- def token(self):
- return self.auth_provider.auth_data[0]
-
def create_container(
self, container_name,
metadata=None,
@@ -185,7 +181,7 @@
url += '?'
url += '&%s' % urllib.urlencode(params)
- resp, body = self.get(url)
+ resp, body = self.get(url, headers={})
if params and params.get('format') == 'json':
body = json.loads(body)
elif params and params.get('format') == 'xml':
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index ca4f1c1..79c5719 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -51,7 +51,7 @@
url = "%s/%s" % (str(container), str(object_name))
if params:
url += '?%s' % urllib.urlencode(params)
- resp, body = self.delete(url)
+ resp, body = self.delete(url, headers={})
return resp, body
def update_object_metadata(self, container, object_name, metadata,
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
index 0a16b9f..b70b2e8 100644
--- a/tempest/services/orchestration/json/orchestration_client.py
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -177,7 +177,7 @@
stack_name = body['stack_name']
stack_status = body['stack_status']
if stack_status == status:
- return
+ return body
if fail_regexp.search(stack_status):
raise exceptions.StackBuildErrorException(
stack_identifier=stack_identifier,
diff --git a/tempest/services/volume/v2/__init__.py b/tempest/services/volume/v2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/v2/__init__.py
diff --git a/tempest/services/volume/v2/json/__init__.py b/tempest/services/volume/v2/json/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/v2/json/__init__.py
diff --git a/tempest/services/volume/v2/json/volumes_client.py b/tempest/services/volume/v2/json/volumes_client.py
new file mode 100644
index 0000000..0524212
--- /dev/null
+++ b/tempest/services/volume/v2/json/volumes_client.py
@@ -0,0 +1,302 @@
+# Copyright 2012 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.
+
+import json
+import time
+import urllib
+
+from tempest.common.rest_client import RestClient
+from tempest import config
+from tempest import exceptions
+
+CONF = config.CONF
+
+
+class VolumesClientJSON(RestClient):
+ """
+ Client class to send CRUD Volume API requests to a Cinder endpoint
+ """
+
+ def __init__(self, auth_provider):
+ super(VolumesClientJSON, self).__init__(auth_provider)
+
+ self.service = CONF.volume.catalog_type
+ self.build_interval = CONF.volume.build_interval
+ self.build_timeout = CONF.volume.build_timeout
+
+ def get_attachment_from_volume(self, volume):
+ """Return the element 'attachment' from input volumes."""
+ return volume['attachments'][0]
+
+ def list_volumes(self, params=None):
+ """List all the volumes created."""
+ url = 'volumes'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['volumes']
+
+ def list_volumes_with_detail(self, params=None):
+ """List the details of all volumes."""
+ url = 'volumes/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['volumes']
+
+ def get_volume(self, volume_id):
+ """Returns the details of a single volume."""
+ url = "volumes/%s" % str(volume_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['volume']
+
+ def create_volume(self, size, **kwargs):
+ """
+ Creates a new Volume.
+ size(Required): Size of volume in GB.
+ Following optional keyword arguments are accepted:
+ display_name: Optional Volume Name.
+ metadata: A dictionary of values to be used as metadata.
+ volume_type: Optional Name of volume_type for the volume
+ snapshot_id: When specified the volume is created from this snapshot
+ imageRef: When specified the volume is created from this image
+ """
+ post_body = {'size': size}
+ post_body.update(kwargs)
+ post_body = json.dumps({'volume': post_body})
+ resp, body = self.post('volumes', post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['volume']
+
+ def update_volume(self, volume_id, **kwargs):
+ """Updates the Specified Volume."""
+ put_body = json.dumps({'volume': kwargs})
+ resp, body = self.put('volumes/%s' % volume_id, put_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['volume']
+
+ def delete_volume(self, volume_id):
+ """Deletes the Specified Volume."""
+ return self.delete("volumes/%s" % str(volume_id))
+
+ def upload_volume(self, volume_id, image_name, disk_format):
+ """Uploads a volume in Glance."""
+ post_body = {
+ 'image_name': image_name,
+ 'disk_format': disk_format
+ }
+ post_body = json.dumps({'os-volume_upload_image': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['os-volume_upload_image']
+
+ def attach_volume(self, volume_id, instance_uuid, mountpoint):
+ """Attaches a volume to a given instance on a given mountpoint."""
+ post_body = {
+ 'instance_uuid': instance_uuid,
+ 'mountpoint': mountpoint,
+ }
+ post_body = json.dumps({'os-attach': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def detach_volume(self, volume_id):
+ """Detaches a volume from an instance."""
+ post_body = {}
+ post_body = json.dumps({'os-detach': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def reserve_volume(self, volume_id):
+ """Reserves a volume."""
+ post_body = {}
+ post_body = json.dumps({'os-reserve': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def unreserve_volume(self, volume_id):
+ """Restore a reserved volume ."""
+ post_body = {}
+ post_body = json.dumps({'os-unreserve': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def wait_for_volume_status(self, volume_id, status):
+ """Waits for a Volume to reach a given status."""
+ resp, body = self.get_volume(volume_id)
+ volume_name = body['display_name']
+ volume_status = body['status']
+ start = int(time.time())
+
+ while volume_status != status:
+ time.sleep(self.build_interval)
+ resp, body = self.get_volume(volume_id)
+ volume_status = body['status']
+ if volume_status == 'error':
+ raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
+
+ if int(time.time()) - start >= self.build_timeout:
+ message = ('Volume %s failed to reach %s status within '
+ 'the required time (%s s).' %
+ (volume_name, status, self.build_timeout))
+ raise exceptions.TimeoutException(message)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume(id)
+ except exceptions.NotFound:
+ return True
+ return False
+
+ def extend_volume(self, volume_id, extend_size):
+ """Extend a volume."""
+ post_body = {
+ 'new_size': extend_size
+ }
+ post_body = json.dumps({'os-extend': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def reset_volume_status(self, volume_id, status):
+ """Reset the Specified Volume's Status."""
+ post_body = json.dumps({'os-reset_status': {"status": status}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
+
+ def volume_begin_detaching(self, volume_id):
+ """Volume Begin Detaching."""
+ post_body = json.dumps({'os-begin_detaching': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
+
+ def volume_roll_detaching(self, volume_id):
+ """Volume Roll Detaching."""
+ post_body = json.dumps({'os-roll_detaching': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
+
+ def create_volume_transfer(self, vol_id, display_name=None):
+ """Create a volume transfer."""
+ post_body = {
+ 'volume_id': vol_id
+ }
+ if display_name:
+ post_body['name'] = display_name
+ post_body = json.dumps({'transfer': post_body})
+ resp, body = self.post('os-volume-transfer',
+ post_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['transfer']
+
+ def get_volume_transfer(self, transfer_id):
+ """Returns the details of a volume transfer."""
+ url = "os-volume-transfer/%s" % str(transfer_id)
+ resp, body = self.get(url, self.headers)
+ body = json.loads(body)
+ return resp, body['transfer']
+
+ def list_volume_transfers(self, params=None):
+ """List all the volume transfers created."""
+ url = 'os-volume-transfer'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['transfers']
+
+ def delete_volume_transfer(self, transfer_id):
+ """Delete a volume transfer."""
+ return self.delete("os-volume-transfer/%s" % str(transfer_id))
+
+ def accept_volume_transfer(self, transfer_id, transfer_auth_key):
+ """Accept a volume transfer."""
+ post_body = {
+ 'auth_key': transfer_auth_key,
+ }
+ url = 'os-volume-transfer/%s/accept' % transfer_id
+ post_body = json.dumps({'accept': post_body})
+ resp, body = self.post(url, post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['transfer']
+
+ def update_volume_readonly(self, volume_id, readonly):
+ """Update the Specified Volume readonly."""
+ post_body = {
+ 'readonly': readonly
+ }
+ post_body = json.dumps({'os-update_readonly_flag': post_body})
+ url = 'volumes/%s/action' % (volume_id)
+ resp, body = self.post(url, post_body, self.headers)
+ return resp, body
+
+ def force_delete_volume(self, volume_id):
+ """Force Delete Volume."""
+ post_body = json.dumps({'os-force_delete': {}})
+ resp, body = self.post('volumes/%s/action' % volume_id, post_body,
+ self.headers)
+ return resp, body
+
+ def create_volume_metadata(self, volume_id, metadata):
+ """Create metadata for the volume."""
+ put_body = json.dumps({'metadata': metadata})
+ url = "volumes/%s/metadata" % str(volume_id)
+ resp, body = self.post(url, put_body, self.headers)
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def get_volume_metadata(self, volume_id):
+ """Get metadata of the volume."""
+ url = "volumes/%s/metadata" % str(volume_id)
+ resp, body = self.get(url, self.headers)
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def update_volume_metadata(self, volume_id, metadata):
+ """Update metadata for the volume."""
+ put_body = json.dumps({'metadata': metadata})
+ url = "volumes/%s/metadata" % str(volume_id)
+ resp, body = self.put(url, put_body, self.headers)
+ body = json.loads(body)
+ return resp, body['metadata']
+
+ def update_volume_metadata_item(self, volume_id, id, meta_item):
+ """Update metadata item for the volume."""
+ put_body = json.dumps({'meta': meta_item})
+ url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+ resp, body = self.put(url, put_body, self.headers)
+ body = json.loads(body)
+ return resp, body['meta']
+
+ def delete_volume_metadata_item(self, volume_id, id):
+ """Delete metadata item for the volume."""
+ url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+ resp, body = self.delete(url, self.headers)
+ return resp, body
diff --git a/tempest/services/volume/v2/xml/__init__.py b/tempest/services/volume/v2/xml/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/v2/xml/__init__.py
diff --git a/tempest/services/volume/v2/xml/volumes_client.py b/tempest/services/volume/v2/xml/volumes_client.py
new file mode 100644
index 0000000..deb56fd
--- /dev/null
+++ b/tempest/services/volume/v2/xml/volumes_client.py
@@ -0,0 +1,412 @@
+# Copyright 2012 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 time
+import urllib
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import config
+from tempest import exceptions
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+CONF = config.CONF
+
+
+class VolumesClientXML(RestClientXML):
+ """
+ Client class to send CRUD Volume API requests to a Cinder endpoint
+ """
+
+ def __init__(self, auth_provider):
+ super(VolumesClientXML, self).__init__(auth_provider)
+ self.service = CONF.volume.catalog_type
+ self.build_interval = CONF.compute.build_interval
+ self.build_timeout = CONF.compute.build_timeout
+
+ def _parse_volume(self, body):
+ vol = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ if tag == 'metadata':
+ vol['metadata'] = dict((meta.get('key'),
+ meta.text) for meta in
+ child.getchildren())
+ else:
+ vol[tag] = xml_to_json(child)
+ return vol
+
+ def get_attachment_from_volume(self, volume):
+ """Return the element 'attachment' from input volumes."""
+ return volume['attachments']['attachment']
+
+ def _check_if_bootable(self, volume):
+ """
+ Check if the volume is bootable, also change the value
+ of 'bootable' from string to boolean.
+ """
+
+ # NOTE(jdg): Version 1 of Cinder API uses lc strings
+ # We should consider being explicit in this check to
+ # avoid introducing bugs like: LP #1227837
+
+ if volume['bootable'].lower() == 'true':
+ volume['bootable'] = True
+ elif volume['bootable'].lower() == 'false':
+ volume['bootable'] = False
+ else:
+ raise ValueError(
+ 'bootable flag is supposed to be either True or False,'
+ 'it is %s' % volume['bootable'])
+ return volume
+
+ def list_volumes(self, params=None):
+ """List all the volumes created."""
+ url = 'volumes'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ for v in volumes:
+ v = self._check_if_bootable(v)
+ return resp, volumes
+
+ def list_volumes_with_detail(self, params=None):
+ """List all the details of volumes."""
+ url = 'volumes/detail'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ for v in volumes:
+ v = self._check_if_bootable(v)
+ return resp, volumes
+
+ def get_volume(self, volume_id):
+ """Returns the details of a single volume."""
+ url = "volumes/%s" % str(volume_id)
+ resp, body = self.get(url, self.headers)
+ body = self._parse_volume(etree.fromstring(body))
+ body = self._check_if_bootable(body)
+ return resp, body
+
+ def create_volume(self, size, **kwargs):
+ """Creates a new Volume.
+
+ :param size: Size of volume in GB. (Required)
+ :param display_name: Optional Volume Name.
+ :param metadata: An optional dictionary of values for metadata.
+ :param volume_type: Optional Name of volume_type for the volume
+ :param snapshot_id: When specified the volume is created from
+ this snapshot
+ :param imageRef: When specified the volume is created from this
+ image
+ """
+ # NOTE(afazekas): it should use a volume namespace
+ volume = Element("volume", xmlns=XMLNS_11, size=size)
+
+ if 'metadata' in kwargs:
+ _metadata = Element('metadata')
+ volume.append(_metadata)
+ for key, value in kwargs['metadata'].items():
+ meta = Element('meta')
+ meta.add_attr('key', key)
+ meta.append(Text(value))
+ _metadata.append(meta)
+ attr_to_add = kwargs.copy()
+ del attr_to_add['metadata']
+ else:
+ attr_to_add = kwargs
+
+ for key, value in attr_to_add.items():
+ volume.add_attr(key, value)
+
+ resp, body = self.post('volumes', str(Document(volume)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def update_volume(self, volume_id, **kwargs):
+ """Updates the Specified Volume."""
+ put_body = Element("volume", xmlns=XMLNS_11, **kwargs)
+
+ resp, body = self.put('volumes/%s' % volume_id,
+ str(Document(put_body)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume(self, volume_id):
+ """Deletes the Specified Volume."""
+ return self.delete("volumes/%s" % str(volume_id))
+
+ def wait_for_volume_status(self, volume_id, status):
+ """Waits for a Volume to reach a given status."""
+ resp, body = self.get_volume(volume_id)
+ volume_status = body['status']
+ start = int(time.time())
+
+ while volume_status != status:
+ time.sleep(self.build_interval)
+ resp, body = self.get_volume(volume_id)
+ volume_status = body['status']
+ if volume_status == 'error':
+ raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
+
+ if int(time.time()) - start >= self.build_timeout:
+ message = 'Volume %s failed to reach %s status within '\
+ 'the required time (%s s).' % (volume_id,
+ status,
+ self.build_timeout)
+ raise exceptions.TimeoutException(message)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume(id)
+ except exceptions.NotFound:
+ return True
+ return False
+
+ def attach_volume(self, volume_id, instance_uuid, mountpoint):
+ """Attaches a volume to a given instance on a given mountpoint."""
+ post_body = Element("os-attach",
+ instance_uuid=instance_uuid,
+ mountpoint=mountpoint
+ )
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def detach_volume(self, volume_id):
+ """Detaches a volume from an instance."""
+ post_body = Element("os-detach")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def upload_volume(self, volume_id, image_name, disk_format):
+ """Uploads a volume in Glance."""
+ post_body = Element("os-volume_upload_image",
+ image_name=image_name,
+ disk_format=disk_format)
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ volume = xml_to_json(etree.fromstring(body))
+ return resp, volume
+
+ def extend_volume(self, volume_id, extend_size):
+ """Extend a volume."""
+ post_body = Element("os-extend",
+ new_size=extend_size)
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def reset_volume_status(self, volume_id, status):
+ """Reset the Specified Volume's Status."""
+ post_body = Element("os-reset_status",
+ status=status
+ )
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def volume_begin_detaching(self, volume_id):
+ """Volume Begin Detaching."""
+ post_body = Element("os-begin_detaching")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def volume_roll_detaching(self, volume_id):
+ """Volume Roll Detaching."""
+ post_body = Element("os-roll_detaching")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def reserve_volume(self, volume_id):
+ """Reserves a volume."""
+ post_body = Element("os-reserve")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def unreserve_volume(self, volume_id):
+ """Restore a reserved volume ."""
+ post_body = Element("os-unreserve")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def create_volume_transfer(self, vol_id, display_name=None):
+ """Create a volume transfer."""
+ post_body = Element("transfer",
+ volume_id=vol_id)
+ if display_name:
+ post_body.add_attr('name', display_name)
+ resp, body = self.post('os-volume-transfer',
+ str(Document(post_body)),
+ self.headers)
+ volume = xml_to_json(etree.fromstring(body))
+ return resp, volume
+
+ def get_volume_transfer(self, transfer_id):
+ """Returns the details of a volume transfer."""
+ url = "os-volume-transfer/%s" % str(transfer_id)
+ resp, body = self.get(url, self.headers)
+ volume = xml_to_json(etree.fromstring(body))
+ return resp, volume
+
+ def list_volume_transfers(self, params=None):
+ """List all the volume transfers created."""
+ url = 'os-volume-transfer'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume_transfer(vol) for vol in list(body)]
+ return resp, volumes
+
+ def _parse_volume_transfer(self, body):
+ vol = dict((attr, body.get(attr)) for attr in body.keys())
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ tag = tag.split("}", 1)
+ vol[tag] = xml_to_json(child)
+ return vol
+
+ def delete_volume_transfer(self, transfer_id):
+ """Delete a volume transfer."""
+ return self.delete("os-volume-transfer/%s" % str(transfer_id))
+
+ def accept_volume_transfer(self, transfer_id, transfer_auth_key):
+ """Accept a volume transfer."""
+ post_body = Element("accept", auth_key=transfer_auth_key)
+ url = 'os-volume-transfer/%s/accept' % transfer_id
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ volume = xml_to_json(etree.fromstring(body))
+ return resp, volume
+
+ def update_volume_readonly(self, volume_id, readonly):
+ """Update the Specified Volume readonly."""
+ post_body = Element("os-update_readonly_flag",
+ readonly=readonly)
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def force_delete_volume(self, volume_id):
+ """Force Delete Volume."""
+ post_body = Element("os-force_delete")
+ url = 'volumes/%s/action' % str(volume_id)
+ resp, body = self.post(url, str(Document(post_body)), self.headers)
+ if body:
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def _metadata_body(self, meta):
+ post_body = Element('metadata')
+ for k, v in meta.items():
+ data = Element('meta', key=k)
+ data.append(Text(v))
+ post_body.append(data)
+ return post_body
+
+ def _parse_key_value(self, node):
+ """Parse <foo key='key'>value</foo> data into {'key': 'value'}."""
+ data = {}
+ for node in node.getchildren():
+ data[node.get('key')] = node.text
+ return data
+
+ def create_volume_metadata(self, volume_id, metadata):
+ """Create metadata for the volume."""
+ post_body = self._metadata_body(metadata)
+ resp, body = self.post('volumes/%s/metadata' % volume_id,
+ str(Document(post_body)),
+ self.headers)
+ body = self._parse_key_value(etree.fromstring(body))
+ return resp, body
+
+ def get_volume_metadata(self, volume_id):
+ """Get metadata of the volume."""
+ url = "volumes/%s/metadata" % str(volume_id)
+ resp, body = self.get(url, self.headers)
+ body = self._parse_key_value(etree.fromstring(body))
+ return resp, body
+
+ def update_volume_metadata(self, volume_id, metadata):
+ """Update metadata for the volume."""
+ put_body = self._metadata_body(metadata)
+ url = "volumes/%s/metadata" % str(volume_id)
+ resp, body = self.put(url, str(Document(put_body)), self.headers)
+ body = self._parse_key_value(etree.fromstring(body))
+ return resp, body
+
+ def update_volume_metadata_item(self, volume_id, id, meta_item):
+ """Update metadata item for the volume."""
+ for k, v in meta_item.items():
+ put_body = Element('meta', key=k)
+ put_body.append(Text(v))
+ url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+ resp, body = self.put(url, str(Document(put_body)), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume_metadata_item(self, volume_id, id):
+ """Delete metadata item for the volume."""
+ url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+ return self.delete(url)
diff --git a/tempest/stress/actions/unit_test.py b/tempest/stress/actions/unit_test.py
index 8bd2f22..2f1d28f 100644
--- a/tempest/stress/actions/unit_test.py
+++ b/tempest/stress/actions/unit_test.py
@@ -10,10 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest import config
from tempest.openstack.common import importutils
from tempest.openstack.common import log as logging
import tempest.stress.stressaction as stressaction
+CONF = config.CONF
+
class SetUpClassRunTime(object):
@@ -73,10 +76,14 @@
self.klass.setUpClass()
self.setupclass_called = True
- self.run_core()
-
- if (self.class_setup_per == SetUpClassRunTime.action):
- self.klass.tearDownClass()
+ try:
+ self.run_core()
+ except Exception as e:
+ raise e
+ finally:
+ if (CONF.stress.leave_dirty_stack is False
+ and self.class_setup_per == SetUpClassRunTime.action):
+ self.klass.tearDownClass()
else:
self.run_core()
diff --git a/tempest/test.py b/tempest/test.py
index 49c34cf..8485a48 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -70,16 +70,35 @@
This decorator applies a testtools attr for each service that gets
exercised by a test case.
"""
- valid_service_list = ['compute', 'image', 'volume', 'orchestration',
- 'network', 'identity', 'object_storage', 'dashboard']
+ service_list = {
+ 'compute': CONF.service_available.nova,
+ 'image': CONF.service_available.glance,
+ 'volume': CONF.service_available.cinder,
+ 'orchestration': CONF.service_available.heat,
+ # NOTE(mtreinish) nova-network will provide networking functionality
+ # if neutron isn't available, so always set to True.
+ 'network': True,
+ 'identity': True,
+ 'object_storage': CONF.service_available.swift,
+ 'dashboard': CONF.service_available.horizon,
+ }
def decorator(f):
for service in args:
- if service not in valid_service_list:
+ if service not in service_list:
raise exceptions.InvalidServiceTag('%s is not a valid service'
% service)
attr(type=list(args))(f)
- return f
+
+ @functools.wraps(f)
+ def wrapper(self, *func_args, **func_kwargs):
+ for service in args:
+ if not service_list[service]:
+ msg = 'Skipped because the %s service is not available' % (
+ service)
+ raise testtools.TestCase.skipException(msg)
+ return f(self, *func_args, **func_kwargs)
+ return wrapper
return decorator
diff --git a/tempest/tests/negative/__init__.py b/tempest/tests/negative/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/negative/__init__.py
diff --git a/tempest/tests/negative/test_generate_json.py b/tempest/tests/negative/test_generate_json.py
new file mode 100644
index 0000000..a0aa088
--- /dev/null
+++ b/tempest/tests/negative/test_generate_json.py
@@ -0,0 +1,53 @@
+# Copyright 2014 Deutsche Telekom AG
+# 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.common import generate_json as gen
+import tempest.test
+
+
+class TestGenerateJson(tempest.test.BaseTestCase):
+
+ fake_input_str = {"type": "string",
+ "minLength": 2,
+ "maxLength": 8,
+ 'results': {'gen_number': 404}}
+
+ fake_input_int = {"type": "integer",
+ "maximum": 255,
+ "minimum": 1}
+
+ fake_input_obj = {"type": "object",
+ "properties": {"minRam": {"type": "integer"},
+ "diskName": {"type": "string"},
+ "maxRam": {"type": "integer", }
+ }
+ }
+
+ def _validate_result(self, data):
+ self.assertTrue(isinstance(data, list))
+ for t in data:
+ self.assertTrue(isinstance(t, tuple))
+
+ def test_generate_invalid_string(self):
+ result = gen.generate_invalid(self.fake_input_str)
+ self._validate_result(result)
+
+ def test_generate_invalid_integer(self):
+ result = gen.generate_invalid(self.fake_input_int)
+ self._validate_result(result)
+
+ def test_generate_invalid_obj(self):
+ result = gen.generate_invalid(self.fake_input_obj)
+ self._validate_result(result)
diff --git a/tempest/tests/negative/test_negative_auto_test.py b/tempest/tests/negative/test_negative_auto_test.py
new file mode 100644
index 0000000..4c59383
--- /dev/null
+++ b/tempest/tests/negative/test_negative_auto_test.py
@@ -0,0 +1,64 @@
+# Copyright 2014 Deutsche Telekom AG
+# 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 tempest.test as test
+
+
+class TestNegativeAutoTest(test.BaseTestCase):
+ # Fake entries
+ _interface = 'json'
+ _service = 'compute'
+
+ fake_input_desc = {"name": "list-flavors-with-detail",
+ "http-method": "GET",
+ "url": "flavors/detail",
+ "json-schema": {"type": "object",
+ "properties":
+ {"minRam": {"type": "integer"},
+ "minDisk": {"type": "integer"}}
+ },
+ "resources": ["flavor", "volume", "image"]
+ }
+
+ def _check_prop_entries(self, result, entry):
+ entries = [a for a in result if entry in a[0]]
+ self.assertIsNotNone(entries)
+ self.assertIs(len(entries), 2)
+ for entry in entries:
+ self.assertIsNotNone(entry[1]['schema'])
+
+ def _check_resource_entries(self, result, entry):
+ entries = [a for a in result if entry in a[0]]
+ self.assertIsNotNone(entries)
+ self.assertIs(len(entries), 3)
+ for entry in entries:
+ self.assertIsNotNone(entry[1]['resource'])
+
+ @mock.patch('tempest.test.NegativeAutoTest.load_schema')
+ def test_generate_scenario(self, open_mock):
+ open_mock.return_value = self.fake_input_desc
+ scenarios = test.NegativeAutoTest.\
+ generate_scenario(None)
+
+ self.assertIsInstance(scenarios, list)
+ for scenario in scenarios:
+ self.assertIsInstance(scenario, tuple)
+ self.assertIsInstance(scenario[0], str)
+ self.assertIsInstance(scenario[1], dict)
+ self._check_prop_entries(scenarios, "prop_minRam")
+ self._check_prop_entries(scenarios, "prop_minDisk")
+ self._check_resource_entries(scenarios, "inv_res")
diff --git a/tempest/tests/test_rest_client.py b/tempest/tests/test_rest_client.py
index ead112b..ba43daf 100644
--- a/tempest/tests/test_rest_client.py
+++ b/tempest/tests/test_rest_client.py
@@ -13,11 +13,13 @@
# under the License.
import httplib2
+import json
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
from tempest.openstack.common.fixture import mockpatch
+from tempest.services.compute.xml import common as xml
from tempest.tests import base
from tempest.tests import fake_auth_provider
from tempest.tests import fake_config
@@ -26,6 +28,8 @@
class BaseRestClientTestClass(base.TestCase):
+ url = 'fake_endpoint'
+
def _get_region(self):
return 'fake region'
@@ -49,36 +53,33 @@
'_error_checker'))
def test_post(self):
- __, return_dict = self.rest_client.post('fake_endpoint', {},
- {})
+ __, return_dict = self.rest_client.post(self.url, {}, {})
self.assertEqual('POST', return_dict['method'])
def test_get(self):
- __, return_dict = self.rest_client.get('fake_endpoint')
+ __, return_dict = self.rest_client.get(self.url)
self.assertEqual('GET', return_dict['method'])
def test_delete(self):
- __, return_dict = self.rest_client.delete('fake_endpoint')
+ __, return_dict = self.rest_client.delete(self.url)
self.assertEqual('DELETE', return_dict['method'])
def test_patch(self):
- __, return_dict = self.rest_client.patch('fake_endpoint', {},
- {})
+ __, return_dict = self.rest_client.patch(self.url, {}, {})
self.assertEqual('PATCH', return_dict['method'])
def test_put(self):
- __, return_dict = self.rest_client.put('fake_endpoint', {},
- {})
+ __, return_dict = self.rest_client.put(self.url, {}, {})
self.assertEqual('PUT', return_dict['method'])
def test_head(self):
self.useFixture(mockpatch.PatchObject(self.rest_client,
'response_checker'))
- __, return_dict = self.rest_client.head('fake_endpoint')
+ __, return_dict = self.rest_client.head(self.url)
self.assertEqual('HEAD', return_dict['method'])
def test_copy(self):
- __, return_dict = self.rest_client.copy('fake_endpoint')
+ __, return_dict = self.rest_client.copy(self.url)
self.assertEqual('COPY', return_dict['method'])
@@ -89,4 +90,143 @@
def test_post(self):
self.assertRaises(exceptions.NotFound, self.rest_client.post,
- 'fake_endpoint', {}, {})
+ self.url, {}, {})
+
+
+class TestRestClientHeadersJSON(TestRestClientHTTPMethods):
+ TYPE = "json"
+
+ def _verify_headers(self, resp):
+ self.assertEqual(self.rest_client._get_type(), self.TYPE)
+ resp = dict((k.lower(), v) for k, v in resp.iteritems())
+ self.assertEqual(self.header_value, resp['accept'])
+ self.assertEqual(self.header_value, resp['content-type'])
+
+ def setUp(self):
+ super(TestRestClientHeadersJSON, self).setUp()
+ self.rest_client.TYPE = self.TYPE
+ self.header_value = 'application/%s' % self.rest_client._get_type()
+
+ def test_post(self):
+ resp, __ = self.rest_client.post(self.url, {})
+ self._verify_headers(resp)
+
+ def test_get(self):
+ resp, __ = self.rest_client.get(self.url)
+ self._verify_headers(resp)
+
+ def test_delete(self):
+ resp, __ = self.rest_client.delete(self.url)
+ self._verify_headers(resp)
+
+ def test_patch(self):
+ resp, __ = self.rest_client.patch(self.url, {})
+ self._verify_headers(resp)
+
+ def test_put(self):
+ resp, __ = self.rest_client.put(self.url, {})
+ self._verify_headers(resp)
+
+ def test_head(self):
+ self.useFixture(mockpatch.PatchObject(self.rest_client,
+ 'response_checker'))
+ resp, __ = self.rest_client.head(self.url)
+ self._verify_headers(resp)
+
+ def test_copy(self):
+ resp, __ = self.rest_client.copy(self.url)
+ self._verify_headers(resp)
+
+
+class TestRestClientHeadersXML(TestRestClientHeadersJSON):
+ TYPE = "xml"
+
+ # These two tests are needed in one exemplar
+ def test_send_json_accept_xml(self):
+ resp, __ = self.rest_client.get(self.url,
+ self.rest_client.get_headers("xml",
+ "json"))
+ resp = dict((k.lower(), v) for k, v in resp.iteritems())
+ self.assertEqual("application/json", resp["content-type"])
+ self.assertEqual("application/xml", resp["accept"])
+
+ def test_send_xml_accept_json(self):
+ resp, __ = self.rest_client.get(self.url,
+ self.rest_client.get_headers("json",
+ "xml"))
+ resp = dict((k.lower(), v) for k, v in resp.iteritems())
+ self.assertEqual("application/json", resp["accept"])
+ self.assertEqual("application/xml", resp["content-type"])
+
+
+class TestRestClientParseRespXML(BaseRestClientTestClass):
+ TYPE = "xml"
+
+ keys = ["fake_key1", "fake_key2"]
+ values = ["fake_value1", "fake_value2"]
+ item_expected = {key: value for key, value in zip(keys, values)}
+ list_expected = {"body_list": [
+ {keys[0]: values[0]},
+ {keys[1]: values[1]},
+ ]}
+ dict_expected = {"body_dict": {
+ keys[0]: values[0],
+ keys[1]: values[1],
+ }}
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientParseRespXML, self).setUp()
+ self.rest_client.TYPE = self.TYPE
+
+ def test_parse_resp_body_item(self):
+ body_item = xml.Element("item", **self.item_expected)
+ body = self.rest_client._parse_resp(str(xml.Document(body_item)))
+ self.assertEqual(self.item_expected, body)
+
+ def test_parse_resp_body_list(self):
+ self.rest_client.list_tags = ["fake_list", ]
+ body_list = xml.Element(self.rest_client.list_tags[0])
+ for i in range(2):
+ body_list.append(xml.Element("fake_item",
+ **self.list_expected["body_list"][i]))
+ body = self.rest_client._parse_resp(str(xml.Document(body_list)))
+ self.assertEqual(self.list_expected["body_list"], body)
+
+ def test_parse_resp_body_dict(self):
+ self.rest_client.dict_tags = ["fake_dict", ]
+ body_dict = xml.Element(self.rest_client.dict_tags[0])
+
+ for i in range(2):
+ body_dict.append(xml.Element("fake_item", xml.Text(self.values[i]),
+ key=self.keys[i]))
+
+ body = self.rest_client._parse_resp(str(xml.Document(body_dict)))
+ self.assertEqual(self.dict_expected["body_dict"], body)
+
+
+class TestRestClientParseRespJSON(TestRestClientParseRespXML):
+ TYPE = "json"
+
+ def test_parse_resp_body_item(self):
+ body = self.rest_client._parse_resp(json.dumps(self.item_expected))
+ self.assertEqual(self.item_expected, body)
+
+ def test_parse_resp_body_list(self):
+ body = self.rest_client._parse_resp(json.dumps(self.list_expected))
+ self.assertEqual(self.list_expected["body_list"], body)
+
+ def test_parse_resp_body_dict(self):
+ body = self.rest_client._parse_resp(json.dumps(self.dict_expected))
+ self.assertEqual(self.dict_expected["body_dict"], body)
+
+ def test_parse_resp_two_top_keys(self):
+ dict_two_keys = self.dict_expected.copy()
+ dict_two_keys.update({"second_key": ""})
+ body = self.rest_client._parse_resp(json.dumps(dict_two_keys))
+ self.assertEqual(dict_two_keys, body)
+
+ def test_parse_resp_one_top_key_without_list_or_dict(self):
+ data = {"one_top_key": "not_list_or_dict_value"}
+ body = self.rest_client._parse_resp(json.dumps(data))
+ self.assertEqual(data, body)
diff --git a/tempest/tests/test_ssh.py b/tempest/tests/test_ssh.py
index 429ed56..a6eedc4 100644
--- a/tempest/tests/test_ssh.py
+++ b/tempest/tests/test_ssh.py
@@ -88,15 +88,17 @@
client_mock.connect.side_effect = [socket.error, socket.error, True]
t_mock.side_effect = [
1000, # Start time
+ 1000, # LOG.warning() calls time.time() loop 1
1001, # Sleep loop 1
+ 1001, # LOG.warning() calls time.time() loop 2
1002 # Sleep loop 2
]
client._get_ssh_connection(sleep=1)
expected_sleeps = [
- mock.call(1),
- mock.call(1.01)
+ mock.call(2),
+ mock.call(3)
]
self.assertEqual(expected_sleeps, s_mock.mock_calls)
@@ -111,7 +113,9 @@
]
t_mock.side_effect = [
1000, # Start time
+ 1000, # LOG.warning() calls time.time() loop 1
1001, # Sleep loop 1
+ 1001, # LOG.warning() calls time.time() loop 2
1002, # Sleep loop 2
1003, # Sleep loop 3
1004 # LOG.error() calls time.time()
diff --git a/tools/check_logs.py b/tools/check_logs.py
index f3204e3..15988a6 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -28,7 +28,7 @@
is_neutron = os.environ.get('DEVSTACK_GATE_NEUTRON', "0") == "1"
is_grenade = (os.environ.get('DEVSTACK_GATE_GRENADE', "0") == "1" or
os.environ.get('DEVSTACK_GATE_GRENADE_FORWARD', "0") == "1")
-dump_all_errors = is_neutron
+dump_all_errors = True
def process_files(file_specs, url_specs, whitelists):
@@ -69,6 +69,7 @@
print_log_name = False
if not whitelisted:
had_errors = True
+ print("*** Not Whitelisted ***"),
print(line)
return had_errors
diff --git a/tox.ini b/tox.ini
index 88f2537..1580b14 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,9 +6,6 @@
[testenv]
sitepackages = True
setenv = VIRTUAL_ENV={envdir}
- LANG=en_US.UTF-8
- LANGUAGE=en_US:en
- LC_ALL=C
OS_TEST_PATH=./tempest/test_discover
usedevelop = True
install_command = pip install {opts} {packages}
@@ -40,6 +37,12 @@
commands =
bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty|cli)) {posargs}'
+[testenv:full-serial]
+# The regex below is used to select which tests to run and exclude the slow tag:
+# See the testrepostiory bug: https://bugs.launchpad.net/testrepository/+bug/1208610
+commands =
+ bash tools/pretty_tox_serial.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty|cli)) {posargs}'
+
[testenv:testr-full]
commands =
bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty|cli)) {posargs}'