Merge "Validate list_keypair attribute of Nova V2/V3 APIs"
diff --git a/requirements.txt b/requirements.txt
index 434e12e..a18b092 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,7 @@
python-neutronclient>=2.3.4,<3
python-cinderclient>=1.0.6
python-heatclient>=0.2.3
-python-savannaclient>=0.5.0
+python-saharaclient>=0.6.0
python-swiftclient>=1.6
testresources>=0.2.4
keyring>=1.6.1,<2.0,>=2.1
diff --git a/tempest/api/compute/admin/test_flavors.py b/tempest/api/compute/admin/test_flavors.py
index 05b763a..759585e 100644
--- a/tempest/api/compute/admin/test_flavors.py
+++ b/tempest/api/compute/admin/test_flavors.py
@@ -170,10 +170,12 @@
flag = True
self.assertTrue(flag)
+ @test.skip_because(bug='1286297')
@test.attr(type='gate')
def test_list_non_public_flavor(self):
- # Create a flavor with os-flavor-access:is_public false should
- # be present in list_details.
+ # Create a flavor with os-flavor-access:is_public false.
+ # The flavor should not be present in list_details as the
+ # tenant is not automatically added access list.
# This operation requires the user to have 'admin' role
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
new_flavor_id = data_utils.rand_int_id(start=1000)
@@ -192,7 +194,7 @@
for flavor in flavors:
if flavor['name'] == flavor_name:
flag = True
- self.assertTrue(flag)
+ self.assertFalse(flag)
# Verify flavor is not retrieved with other user
flag = False
diff --git a/tempest/api/compute/admin/test_flavors_access.py b/tempest/api/compute/admin/test_flavors_access.py
index 4804ce4..aa0138f 100644
--- a/tempest/api/compute/admin/test_flavors_access.py
+++ b/tempest/api/compute/admin/test_flavors_access.py
@@ -46,9 +46,11 @@
cls.vcpus = 1
cls.disk = 10
+ @test.skip_because(bug='1286297')
@test.attr(type='gate')
def test_flavor_access_list_with_private_flavor(self):
- # Test to list flavor access successfully by querying private flavor
+ # Test to make sure that list flavor access on a newly created
+ # private flavor will return an empty access list
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
new_flavor_id = data_utils.rand_int_id(start=1000)
resp, new_flavor = self.client.create_flavor(flavor_name,
@@ -60,10 +62,7 @@
self.assertEqual(resp.status, 200)
resp, flavor_access = self.client.list_flavor_access(new_flavor_id)
self.assertEqual(resp.status, 200)
- self.assertEqual(len(flavor_access), 1, str(flavor_access))
- first_flavor = flavor_access[0]
- self.assertEqual(str(new_flavor_id), str(first_flavor['flavor_id']))
- self.assertEqual(self.adm_tenant_id, first_flavor['tenant_id'])
+ self.assertEqual(len(flavor_access), 0, str(flavor_access))
@test.attr(type='gate')
def test_flavor_access_add_remove(self):
diff --git a/tempest/api/compute/admin/test_flavors_negative.py b/tempest/api/compute/admin/test_flavors_negative.py
index b882ff4..162e419 100644
--- a/tempest/api/compute/admin/test_flavors_negative.py
+++ b/tempest/api/compute/admin/test_flavors_negative.py
@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testscenarios
import uuid
from tempest.api.compute import base
@@ -21,7 +20,7 @@
from tempest import exceptions
from tempest import test
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test.NegativeAutoTest.load_tests
class FlavorsAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -108,8 +107,6 @@
_service = 'compute'
_schema_file = 'compute/admin/flavor_create.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@test.attr(type=['negative', 'gate'])
def test_create_flavor(self):
# flavor details are not returned for non-existent flavors
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index 4ba5023..a81b7d9 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -13,13 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testscenarios
from tempest.api.compute import base
from tempest import test
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test.NegativeAutoTest.load_tests
class FlavorsListNegativeTestJSON(base.BaseV2ComputeTest,
@@ -27,8 +26,6 @@
_service = 'compute'
_schema_file = 'compute/flavors/flavors_list.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@test.attr(type=['negative', 'gate'])
def test_list_flavors_with_detail(self):
self.execute(self._schema_file)
@@ -39,8 +36,6 @@
_service = 'compute'
_schema_file = 'compute/flavors/flavor_details.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@classmethod
def setUpClass(cls):
super(FlavorDetailsNegativeTestJSON, cls).setUpClass()
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 26a75a2..72ccc71 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -14,7 +14,6 @@
# under the License.
import base64
-import time
import testtools
import urlparse
@@ -222,18 +221,8 @@
self.client.revert_resize(self.server_id)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
- # Need to poll for the id change until lp#924371 is fixed
resp, server = self.client.get_server(self.server_id)
- start = int(time.time())
-
- while server['flavor']['id'] != previous_flavor_ref:
- time.sleep(self.build_interval)
- resp, server = self.client.get_server(self.server_id)
-
- if int(time.time()) - start >= self.build_timeout:
- message = 'Server %s failed to revert resize within the \
- required time (%s s).' % (self.server_id, self.build_timeout)
- raise exceptions.TimeoutException(message)
+ self.assertEqual(previous_flavor_ref, server['flavor']['id'])
@test.attr(type='gate')
def test_create_backup(self):
diff --git a/tempest/api/compute/servers/test_servers_negative_new.py b/tempest/api/compute/servers/test_servers_negative_new.py
index 42ace76..f860ff9 100644
--- a/tempest/api/compute/servers/test_servers_negative_new.py
+++ b/tempest/api/compute/servers/test_servers_negative_new.py
@@ -13,13 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testscenarios
from tempest.api.compute import base
from tempest import test
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test.NegativeAutoTest.load_tests
class GetConsoleOutputNegativeTestJSON(base.BaseV2ComputeTest,
@@ -27,8 +26,6 @@
_service = 'compute'
_schema_file = 'compute/servers/get_console_output.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@classmethod
def setUpClass(cls):
super(GetConsoleOutputNegativeTestJSON, cls).setUpClass()
diff --git a/tempest/api/compute/v3/flavors/test_flavors_negative.py b/tempest/api/compute/v3/flavors/test_flavors_negative.py
index 346f6d6..1c0e4fb 100644
--- a/tempest/api/compute/v3/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/v3/flavors/test_flavors_negative.py
@@ -13,13 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
-import testscenarios
-
from tempest.api.compute import base
from tempest import test
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test.NegativeAutoTest.load_tests
class FlavorsListNegativeV3Test(base.BaseV3ComputeTest,
@@ -27,8 +25,6 @@
_service = 'computev3'
_schema_file = 'compute/flavors/flavors_list_v3.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@test.attr(type=['negative', 'gate'])
def test_list_flavors_with_detail(self):
self.execute(self._schema_file)
@@ -39,8 +35,6 @@
_service = 'computev3'
_schema_file = 'compute/flavors/flavor_details_v3.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
-
@classmethod
def setUpClass(cls):
super(FlavorDetailsNegativeV3Test, cls).setUpClass()
diff --git a/tempest/api/compute/v3/servers/test_server_actions.py b/tempest/api/compute/v3/servers/test_server_actions.py
index 555d028..2582fa8 100644
--- a/tempest/api/compute/v3/servers/test_server_actions.py
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import time
-
import testtools
from tempest.api.compute import base
@@ -212,18 +210,8 @@
self.client.revert_resize(self.server_id)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
- # Need to poll for the id change until lp#924371 is fixed
resp, server = self.client.get_server(self.server_id)
- start = int(time.time())
-
- while server['flavor']['id'] != previous_flavor_ref:
- time.sleep(self.build_interval)
- resp, server = self.client.get_server(self.server_id)
-
- if int(time.time()) - start >= self.build_timeout:
- message = 'Server %s failed to revert resize within the \
- required time (%s s).' % (self.server_id, self.build_timeout)
- raise exceptions.TimeoutException(message)
+ self.assertEqual(previous_flavor_ref, server['flavor']['id'])
@test.attr(type='gate')
def test_create_backup(self):
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index 3503aa0..f92ad68 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -90,7 +90,7 @@
cls.client.delete_vpnservice(vpnservice['id'])
# Clean up floating IPs
for floating_ip in cls.floating_ips:
- cls.client.delete_floating_ip(floating_ip['id'])
+ cls.client.delete_floatingip(floating_ip['id'])
# Clean up routers
for router in cls.routers:
resp, body = cls.client.list_router_interfaces(router['id'])
@@ -193,11 +193,10 @@
return router
@classmethod
- def create_floating_ip(cls, external_network_id, **kwargs):
+ def create_floatingip(cls, external_network_id):
"""Wrapper utility that returns a test floating IP."""
- resp, body = cls.client.create_floating_ip(
- external_network_id,
- **kwargs)
+ resp, body = cls.client.create_floatingip(
+ floating_network_id=external_network_id)
fip = body['floatingip']
cls.floating_ips.append(fip)
return fip
diff --git a/tempest/api/network/test_floating_ips.py b/tempest/api/network/test_floating_ips.py
index b31c090..06871ad 100644
--- a/tempest/api/network/test_floating_ips.py
+++ b/tempest/api/network/test_floating_ips.py
@@ -65,8 +65,12 @@
@test.attr(type='smoke')
def test_create_list_show_update_delete_floating_ip(self):
# Creates a floating IP
- created_floating_ip = self.create_floating_ip(
- self.ext_net_id, port_id=self.ports[0]['id'])
+ resp, body = self.client.create_floatingip(
+ floating_network_id=self.ext_net_id, port_id=self.ports[0]['id'])
+ self.assertEqual('201', resp['status'])
+ created_floating_ip = body['floatingip']
+ self.addCleanup(self.client.delete_floatingip,
+ created_floating_ip['id'])
self.assertIsNotNone(created_floating_ip['id'])
self.assertIsNotNone(created_floating_ip['tenant_id'])
self.assertIsNotNone(created_floating_ip['floating_ip_address'])
@@ -74,7 +78,7 @@
self.assertEqual(created_floating_ip['floating_network_id'],
self.ext_net_id)
# Verifies the details of a floating_ip
- resp, floating_ip = self.client.show_floating_ip(
+ resp, floating_ip = self.client.show_floatingip(
created_floating_ip['id'])
self.assertEqual('200', resp['status'])
shown_floating_ip = floating_ip['floatingip']
@@ -95,7 +99,7 @@
floatingip_id_list.append(f['id'])
self.assertIn(created_floating_ip['id'], floatingip_id_list)
# Associate floating IP to the other port
- resp, floating_ip = self.client.update_floating_ip(
+ resp, floating_ip = self.client.update_floatingip(
created_floating_ip['id'], port_id=self.ports[1]['id'])
self.assertEqual('200', resp['status'])
updated_floating_ip = floating_ip['floatingip']
@@ -105,7 +109,7 @@
self.assertEqual(updated_floating_ip['router_id'], self.router['id'])
# Disassociate floating IP from the port
- resp, floating_ip = self.client.update_floating_ip(
+ resp, floating_ip = self.client.update_floatingip(
created_floating_ip['id'], port_id=None)
self.assertEqual('200', resp['status'])
updated_floating_ip = floating_ip['floatingip']
@@ -116,17 +120,22 @@
@test.attr(type='smoke')
def test_floating_ip_delete_port(self):
# Create a floating IP
- created_floating_ip = self.create_floating_ip(self.ext_net_id)
+ resp, body = self.client.create_floatingip(
+ floating_network_id=self.ext_net_id)
+ self.assertEqual('201', resp['status'])
+ created_floating_ip = body['floatingip']
+ self.addCleanup(self.client.delete_floatingip,
+ created_floating_ip['id'])
# Create a port
resp, port = self.client.create_port(network_id=self.network['id'])
created_port = port['port']
- resp, floating_ip = self.client.update_floating_ip(
+ resp, floating_ip = self.client.update_floatingip(
created_floating_ip['id'], port_id=created_port['id'])
self.assertEqual('200', resp['status'])
# Delete port
self.client.delete_port(created_port['id'])
# Verifies the details of the floating_ip
- resp, floating_ip = self.client.show_floating_ip(
+ resp, floating_ip = self.client.show_floatingip(
created_floating_ip['id'])
self.assertEqual('200', resp['status'])
shown_floating_ip = floating_ip['floatingip']
@@ -139,8 +148,12 @@
@test.attr(type='smoke')
def test_floating_ip_update_different_router(self):
# Associate a floating IP to a port on a router
- created_floating_ip = self.create_floating_ip(
- self.ext_net_id, port_id=self.ports[1]['id'])
+ resp, body = self.client.create_floatingip(
+ floating_network_id=self.ext_net_id, port_id=self.ports[1]['id'])
+ self.assertEqual('201', resp['status'])
+ created_floating_ip = body['floatingip']
+ self.addCleanup(self.client.delete_floatingip,
+ created_floating_ip['id'])
self.assertEqual(created_floating_ip['router_id'], self.router['id'])
network2 = self.create_network()
subnet2 = self.create_subnet(network2)
@@ -149,7 +162,7 @@
self.create_router_interface(router2['id'], subnet2['id'])
port_other_router = self.create_port(network2)
# Associate floating IP to the other port on another router
- resp, floating_ip = self.client.update_floating_ip(
+ resp, floating_ip = self.client.update_floatingip(
created_floating_ip['id'], port_id=port_other_router['id'])
self.assertEqual('200', resp['status'])
updated_floating_ip = floating_ip['floatingip']
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index 3424082..9bf9568 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -13,6 +13,7 @@
from tempest import clients
from tempest.common.utils import data_utils
from tempest import config
+from tempest import exceptions
from tempest.openstack.common import log as logging
import tempest.test
@@ -35,6 +36,7 @@
cls.os = os
cls.orchestration_client = os.orchestration_client
+ cls.client = os.orchestration_client
cls.servers_client = os.servers_client
cls.keypairs_client = os.keypairs_client
cls.network_client = os.network_client
@@ -72,15 +74,15 @@
def clear_stacks(cls):
for stack_identifier in cls.stacks:
try:
- cls.orchestration_client.delete_stack(stack_identifier)
- except Exception:
+ cls.client.delete_stack(stack_identifier)
+ except exceptions.NotFound:
pass
for stack_identifier in cls.stacks:
try:
- cls.orchestration_client.wait_for_stack_status(
+ cls.client.wait_for_stack_status(
stack_identifier, 'DELETE_COMPLETE')
- except Exception:
+ except exceptions.NotFound:
pass
@classmethod
diff --git a/tempest/api/orchestration/stacks/test_limits.py b/tempest/api/orchestration/stacks/test_limits.py
index 22f544d..893dcc4 100644
--- a/tempest/api/orchestration/stacks/test_limits.py
+++ b/tempest/api/orchestration/stacks/test_limits.py
@@ -24,16 +24,10 @@
class TestServerStackLimits(base.BaseOrchestrationTest):
- _interface = 'json'
-
- @classmethod
- def setUpClass(cls):
- super(TestServerStackLimits, cls).setUpClass()
- cls.client = cls.orchestration_client
- cls.stack_name = data_utils.rand_name('heat')
@attr(type='gate')
def test_exceed_max_template_size_fails(self):
+ stack_name = data_utils.rand_name('heat')
fill = 'A' * CONF.orchestration.max_template_size
template = '''
HeatTemplateFormatVersion: '2012-12-12'
@@ -41,5 +35,5 @@
Outputs:
Foo: bar''' % fill
ex = self.assertRaises(exceptions.BadRequest, self.create_stack,
- self.stack_name, template)
+ stack_name, template)
self.assertIn('Template exceeds maximum allowed size', str(ex))
diff --git a/tempest/api/telemetry/test_telemetry_alarming_api.py b/tempest/api/telemetry/test_telemetry_alarming_api.py
index 907d3d0..a59d3ae 100644
--- a/tempest/api/telemetry/test_telemetry_alarming_api.py
+++ b/tempest/api/telemetry/test_telemetry_alarming_api.py
@@ -20,9 +20,29 @@
@attr(type="gate")
def test_alarm_list(self):
- resp, _ = self.telemetry_client.list_alarms()
+ # Create an alarm to verify in the list of alarms
+ created_alarm_ids = list()
+ fetched_ids = list()
+ rules = {'meter_name': 'cpu_util',
+ 'comparison_operator': 'gt',
+ 'threshold': 80.0,
+ 'period': 70}
+ for i in range(3):
+ resp, body = self.create_alarm(threshold_rule=rules)
+ created_alarm_ids.append(body['alarm_id'])
+
+ # List alarms
+ resp, alarm_list = self.telemetry_client.list_alarms()
self.assertEqual(int(resp['status']), 200)
+ # Verify created alarm in the list
+ fetched_ids = [a['alarm_id'] for a in alarm_list]
+ missing_alarms = [a for a in created_alarm_ids if a not in fetched_ids]
+ self.assertEqual(0, len(missing_alarms),
+ "Failed to find the following created alarm(s)"
+ " in a fetched list: %s" %
+ ', '.join(str(a) for a in missing_alarms))
+
@attr(type="gate")
def test_create_alarm(self):
rules = {'meter_name': 'cpu_util',
diff --git a/tempest/api/volume/admin/test_multi_backend.py b/tempest/api/volume/admin/test_multi_backend.py
index 6178a1c..e79d23c 100644
--- a/tempest/api/volume/admin/test_multi_backend.py
+++ b/tempest/api/volume/admin/test_multi_backend.py
@@ -25,10 +25,10 @@
_interface = "json"
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumeMultiBackendTest, cls).setUpClass()
if not CONF.volume_feature_enabled.multi_backend:
- cls.tearDownClass()
raise cls.skipException("Cinder multi-backend feature disabled")
cls.backend1_name = CONF.volume.backend1_name
@@ -37,40 +37,36 @@
cls.volume_client = cls.os_adm.volumes_client
cls.volume_type_id_list = []
cls.volume_id_list = []
- try:
- # Volume/Type creation (uses backend1_name)
- 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.client.create_volume_type(
- type1_name, extra_specs=extra_specs1)
- cls.volume_type_id_list.append(cls.type1['id'])
- resp, cls.volume1 = cls.volume_client.create_volume(
- size=1, display_name=vol1_name, volume_type=type1_name)
- cls.volume_id_list.append(cls.volume1['id'])
- cls.volume_client.wait_for_volume_status(cls.volume1['id'],
+ # Volume/Type creation (uses backend1_name)
+ 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.client.create_volume_type(
+ type1_name, extra_specs=extra_specs1)
+ cls.volume_type_id_list.append(cls.type1['id'])
+
+ resp, cls.volume1 = cls.volume_client.create_volume(
+ size=1, display_name=vol1_name, volume_type=type1_name)
+ cls.volume_id_list.append(cls.volume1['id'])
+ cls.volume_client.wait_for_volume_status(cls.volume1['id'],
+ 'available')
+
+ if cls.backend1_name != cls.backend2_name:
+ # Volume/Type creation (uses backend2_name)
+ 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.client.create_volume_type(
+ type2_name, extra_specs=extra_specs2)
+ cls.volume_type_id_list.append(cls.type2['id'])
+
+ resp, cls.volume2 = cls.volume_client.create_volume(
+ size=1, display_name=vol2_name, volume_type=type2_name)
+ cls.volume_id_list.append(cls.volume2['id'])
+ cls.volume_client.wait_for_volume_status(cls.volume2['id'],
'available')
- if cls.backend1_name != cls.backend2_name:
- # Volume/Type creation (uses backend2_name)
- 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.client.create_volume_type(
- type2_name, extra_specs=extra_specs2)
- cls.volume_type_id_list.append(cls.type2['id'])
-
- resp, cls.volume2 = cls.volume_client.create_volume(
- size=1, display_name=vol2_name, volume_type=type2_name)
- cls.volume_id_list.append(cls.volume2['id'])
- cls.volume_client.wait_for_volume_status(cls.volume2['id'],
- 'available')
- except Exception as e:
- LOG.exception("setup failed: %s" % e)
- cls.tearDownClass()
- raise
-
@classmethod
def tearDownClass(cls):
# volumes deletion
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index cd6d7a8..f9fbe18 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -27,6 +27,7 @@
_interface = "json"
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumesBackupsTest, cls).setUpClass()
diff --git a/tempest/api/volume/test_volume_metadata.py b/tempest/api/volume/test_volume_metadata.py
index e94c700..0d57d47 100644
--- a/tempest/api/volume/test_volume_metadata.py
+++ b/tempest/api/volume/test_volume_metadata.py
@@ -23,16 +23,13 @@
_interface = "json"
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumeMetadataTest, cls).setUpClass()
# Create a volume
cls.volume = cls.create_volume()
cls.volume_id = cls.volume['id']
- @classmethod
- def tearDownClass(cls):
- super(VolumeMetadataTest, cls).tearDownClass()
-
def tearDown(self):
# Update the metadata to {}
self.volumes_client.update_volume_metadata(self.volume_id, {})
diff --git a/tempest/api/volume/test_volumes_list.py b/tempest/api/volume/test_volumes_list.py
index c356342..e2f7a38 100644
--- a/tempest/api/volume/test_volumes_list.py
+++ b/tempest/api/volume/test_volumes_list.py
@@ -56,6 +56,7 @@
[str_vol(v) for v in fetched_list]))
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumesListTest, cls).setUpClass()
cls.client = cls.volumes_client
@@ -65,24 +66,10 @@
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
+ 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'])
@classmethod
def tearDownClass(cls):
diff --git a/tempest/api/volume/test_volumes_negative.py b/tempest/api/volume/test_volumes_negative.py
index 82924a5..a8b0a8d 100644
--- a/tempest/api/volume/test_volumes_negative.py
+++ b/tempest/api/volume/test_volumes_negative.py
@@ -25,6 +25,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumesNegativeTest, cls).setUpClass()
cls.client = cls.volumes_client
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 84c9501..2ce3a4f 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -24,14 +24,10 @@
_interface = "json"
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumesSnapshotTest, cls).setUpClass()
- try:
- cls.volume_origin = cls.create_volume()
- except Exception:
- LOG.exception("setup failed")
- cls.tearDownClass()
- raise
+ cls.volume_origin = cls.create_volume()
@classmethod
def tearDownClass(cls):
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
index 4d2573b..41445d7 100644
--- a/tempest/api/volume/v2/test_volumes_list.py
+++ b/tempest/api/volume/v2/test_volumes_list.py
@@ -56,6 +56,7 @@
[str_vol(v) for v in fetched_list]))
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(VolumesV2ListTestJSON, cls).setUpClass()
cls.client = cls.volumes_client
@@ -65,23 +66,10 @@
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
+ 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'])
@classmethod
def tearDownClass(cls):
diff --git a/tempest/api_schema/compute/aggregates.py b/tempest/api_schema/compute/aggregates.py
new file mode 100644
index 0000000..49793fe
--- /dev/null
+++ b/tempest/api_schema/compute/aggregates.py
@@ -0,0 +1,43 @@
+# Copyright 2014 NEC Corporation. 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.
+
+list_aggregates = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregates': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'availability_zone': {'type': ['string', 'null']},
+ 'created_at': {'type': 'string'},
+ 'deleted': {'type': 'boolean'},
+ 'deleted_at': {'type': ['string', 'null']},
+ 'hosts': {'type': 'array'},
+ 'id': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'name': {'type': 'string'},
+ 'updated_at': {'type': ['string', 'null']}
+ },
+ 'required': ['availability_zone', 'created_at', 'deleted',
+ 'deleted_at', 'hosts', 'id', 'metadata',
+ 'name', 'updated_at']
+ }
+ }
+ },
+ 'required': ['aggregates']
+ }
+}
diff --git a/tempest/api_schema/compute/flavors_access.py b/tempest/api_schema/compute/flavors_access.py
new file mode 100644
index 0000000..152e24c
--- /dev/null
+++ b/tempest/api_schema/compute/flavors_access.py
@@ -0,0 +1,34 @@
+# Copyright 2014 NEC Corporation. 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.
+
+list_flavor_access = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor_access': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'flavor_id': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ },
+ 'required': ['flavor_id', 'tenant_id'],
+ }
+ }
+ },
+ 'required': ['flavor_access']
+ }
+}
diff --git a/tempest/api_schema/compute/parameter_types.py b/tempest/api_schema/compute/parameter_types.py
new file mode 100644
index 0000000..67c0c9b
--- /dev/null
+++ b/tempest/api_schema/compute/parameter_types.py
@@ -0,0 +1,28 @@
+# Copyright 2014 NEC Corporation. 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.
+
+links = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {
+ 'type': 'string',
+ 'format': 'uri'
+ },
+ 'rel': {'type': 'string'}
+ },
+ 'required': ['href', 'rel']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/floating_ips.py b/tempest/api_schema/compute/v2/floating_ips.py
index 61582ec..648d0bf 100644
--- a/tempest/api_schema/compute/v2/floating_ips.py
+++ b/tempest/api_schema/compute/v2/floating_ips.py
@@ -44,3 +44,33 @@
'required': ['floating_ips']
}
}
+
+floating_ip = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ip': {
+ 'type': 'object',
+ 'properties': {
+ # NOTE: Now the type of 'id' is integer, but here allows
+ # 'string' also because we will be able to change it to
+ # 'uuid' in the future.
+ 'id': {'type': ['integer', 'string']},
+ 'pool': {'type': ['string', 'null']},
+ 'instance_id': {'type': ['integer', 'string', 'null']},
+ 'ip': {
+ 'type': 'string',
+ 'format': 'ip-address'
+ },
+ 'fixed_ip': {
+ 'type': ['string', 'null'],
+ 'format': 'ip-address'
+ }
+ },
+ 'required': ['id', 'pool', 'instance_id', 'ip', 'fixed_ip']
+ }
+ },
+ 'required': ['floating_ip']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/images.py b/tempest/api_schema/compute/v2/images.py
index fb4804d..41593c6 100644
--- a/tempest/api_schema/compute/v2/images.py
+++ b/tempest/api_schema/compute/v2/images.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.api_schema.compute import parameter_types
+
get_image = {
'status_code': [200],
'response_body': {
@@ -23,20 +25,7 @@
'id': {'type': 'string'},
'status': {'type': 'string'},
'updated': {'type': 'string'},
- 'links': {
- 'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'rel': {'type': 'string'}
- },
- 'required': ['href', 'rel']
- }
- },
+ 'links': parameter_types.links,
'name': {'type': 'string'},
'created': {'type': 'string'},
'OS-EXT-IMG-SIZE:size': {'type': 'integer'},
@@ -51,20 +40,7 @@
# allows 'string' also because we will be able to
# change it to 'uuid' in the future.
'id': {'type': ['integer', 'string']},
- 'links': {
- 'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'rel': {'type': 'string'}
- },
- 'required': ['href', 'rel']
- }
- }
+ 'links': parameter_types.links
},
'required': ['id', 'links']
}
@@ -79,3 +55,38 @@
'required': ['image']
}
}
+
+list_images = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'images': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {
+ 'type': 'string',
+ 'format': 'uri'
+ },
+ 'rel': {'type': 'string'}
+ },
+ 'required': ['href', 'rel']
+ }
+ },
+ 'name': {'type': 'string'}
+ },
+ 'required': ['id', 'links', 'name']
+ }
+ }
+ },
+ 'required': ['images']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/security_groups.py b/tempest/api_schema/compute/v2/security_groups.py
new file mode 100644
index 0000000..68b65b4
--- /dev/null
+++ b/tempest/api_schema/compute/v2/security_groups.py
@@ -0,0 +1,38 @@
+# Copyright 2014 NEC Corporation. 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.
+
+list_security_groups = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_groups': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'string']},
+ 'name': {'type': 'string'},
+ 'tenant_id': {'type': 'string'},
+ 'rules': {'type': 'array'},
+ 'description': {'type': 'string'},
+ },
+ 'required': ['id', 'name', 'tenant_id', 'rules',
+ 'description'],
+ }
+ }
+ },
+ 'required': ['security_groups']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index 7f06ca6..b4e6e53 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.api_schema.compute import parameter_types
+
create_server = {
'status_code': [202],
'response_body': {
@@ -25,20 +27,7 @@
# as a server id.
'id': {'type': ['integer', 'string']},
'security_groups': {'type': 'array'},
- 'links': {
- 'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'rel': {'type': 'string'}
- },
- 'required': ['href', 'rel']
- }
- },
+ 'links': parameter_types.links,
'adminPass': {'type': 'string'},
'OS-DCF:diskConfig': {'type': 'string'}
},
diff --git a/tempest/api_schema/compute/v2/volumes.py b/tempest/api_schema/compute/v2/volumes.py
index 16ed7c2..9cfd7e3 100644
--- a/tempest/api_schema/compute/v2/volumes.py
+++ b/tempest/api_schema/compute/v2/volumes.py
@@ -20,10 +20,7 @@
'volume': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is integer, but here allows
- # 'string' also because we will be able to change it to
- # 'uuid' in the future.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'status': {'type': 'string'},
'displayName': {'type': ['string', 'null']},
'availabilityZone': {'type': 'string'},
@@ -38,11 +35,17 @@
'items': {
'type': 'object',
'properties': {
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'device': {'type': 'string'},
- 'volumeId': {'type': ['integer', 'string']},
+ 'volumeId': {'type': 'string'},
'serverId': {'type': ['integer', 'string']}
}
+ # NOTE- If volume is not attached to any server
+ # then, 'attachments' attributes comes as array
+ # with empty objects "[{}]" due to that elements
+ # of 'attachments' cannot defined as 'required'.
+ # If it would come as empty array "[]" then,
+ # those elements can be defined as 'required'.
}
}
},
@@ -54,3 +57,54 @@
'required': ['volume']
}
}
+
+list_volumes = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumes': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'status': {'type': 'string'},
+ 'displayName': {'type': ['string', 'null']},
+ 'availabilityZone': {'type': 'string'},
+ 'createdAt': {'type': 'string'},
+ 'displayDescription': {'type': ['string', 'null']},
+ 'volumeType': {'type': 'string'},
+ 'snapshotId': {'type': ['string', 'null']},
+ 'metadata': {'type': 'object'},
+ 'size': {'type': 'integer'},
+ 'attachments': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': ['integer', 'string']}
+ }
+ # NOTE- If volume is not attached to any server
+ # then, 'attachments' attributes comes as array
+ # with empty object "[{}]" due to that elements
+ # of 'attachments' cannot defined as 'required'
+ # If it would come as empty array "[]" then,
+ # those elements can be defined as 'required'.
+ }
+ }
+ },
+ 'required': ['id', 'status', 'displayName',
+ 'availabilityZone', 'createdAt',
+ 'displayDescription', 'volumeType',
+ 'snapshotId', 'metadata', 'size',
+ 'attachments']
+ }
+ }
+ },
+ 'required': ['volumes']
+ }
+}
diff --git a/tempest/api_schema/compute/v3/servers.py b/tempest/api_schema/compute/v3/servers.py
index e69b25f..390962e 100644
--- a/tempest/api_schema/compute/v3/servers.py
+++ b/tempest/api_schema/compute/v3/servers.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.api_schema.compute import parameter_types
+
create_server = {
'status_code': [202],
'response_body': {
@@ -25,20 +27,7 @@
# as a server id.
'id': {'type': ['integer', 'string']},
'os-security-groups:security_groups': {'type': 'array'},
- 'links': {
- 'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'href': {
- 'type': 'string',
- 'format': 'uri'
- },
- 'rel': {'type': 'string'}
- },
- 'required': ['href', 'rel']
- }
- },
+ 'links': parameter_types.links,
'admin_password': {'type': 'string'},
'os-access-ips:access_ip_v4': {'type': 'string'},
'os-access-ips:access_ip_v6': {'type': 'string'}
diff --git a/tempest/auth.py b/tempest/auth.py
index 0e45161..5fc923f 100644
--- a/tempest/auth.py
+++ b/tempest/auth.py
@@ -164,6 +164,8 @@
class KeystoneAuthProvider(AuthProvider):
+ token_expiry_threshold = datetime.timedelta(seconds=60)
+
def __init__(self, credentials, client_type='tempest', interface=None):
super(KeystoneAuthProvider, self).__init__(credentials, client_type,
interface)
@@ -293,7 +295,8 @@
_, access = auth_data
expiry = datetime.datetime.strptime(access['token']['expires'],
self.EXPIRY_DATE_FORMAT)
- return expiry <= datetime.datetime.now()
+ return expiry - self.token_expiry_threshold <= \
+ datetime.datetime.utcnow()
class KeystoneV3AuthProvider(KeystoneAuthProvider):
@@ -393,4 +396,5 @@
_, access = auth_data
expiry = datetime.datetime.strptime(access['expires_at'],
self.EXPIRY_DATE_FORMAT)
- return expiry <= datetime.datetime.now()
+ return expiry - self.token_expiry_threshold <= \
+ datetime.datetime.utcnow()
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index 932b151..6aa98c4 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -93,8 +93,7 @@
"""Executes sahara command for the given action."""
flags += ' --endpoint-type %s' % CONF.data_processing.endpoint_type
return self.cmd_with_auth(
- # TODO (slukjanov): replace with sahara when new client released
- 'savanna', action, flags, params, admin, fail_ok)
+ 'sahara', action, flags, params, admin, fail_ok)
def cmd_with_auth(self, cmd, action, flags='', params='',
admin=True, fail_ok=False):
@@ -115,25 +114,19 @@
cmd = ' '.join([os.path.join(CONF.cli.cli_dir, cmd),
flags, action, params])
LOG.info("running: '%s'" % cmd)
- cmd_str = cmd
cmd = shlex.split(cmd)
result = ''
result_err = ''
- try:
- stdout = subprocess.PIPE
- stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
- proc = subprocess.Popen(
- cmd, stdout=stdout, stderr=stderr)
- result, result_err = proc.communicate()
- if not fail_ok and proc.returncode != 0:
- raise CommandFailed(proc.returncode,
- cmd,
- result,
- stderr=result_err)
- finally:
- LOG.debug('output of %s:\n%s' % (cmd_str, result))
- if not merge_stderr and result_err:
- LOG.debug('error output of %s:\n%s' % (cmd_str, result_err))
+ stdout = subprocess.PIPE
+ stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
+ proc = subprocess.Popen(
+ cmd, stdout=stdout, stderr=stderr)
+ result, result_err = proc.communicate()
+ if not fail_ok and proc.returncode != 0:
+ raise CommandFailed(proc.returncode,
+ cmd,
+ result,
+ stderr=result_err)
return result
def assertTableStruct(self, items, field_names):
diff --git a/tempest/cli/output_parser.py b/tempest/cli/output_parser.py
index 4edcd47..80234a3 100644
--- a/tempest/cli/output_parser.py
+++ b/tempest/cli/output_parser.py
@@ -17,6 +17,7 @@
import re
+from tempest import exceptions
from tempest.openstack.common import log as logging
@@ -37,7 +38,7 @@
for table_ in tables_:
if 'Property' not in table_['headers'] \
or 'Value' not in table_['headers']:
- raise Exception('Invalid structure of table with details')
+ raise exceptions.InvalidStructure()
item = {}
for value in table_['values']:
item[value[0]] = value[1]
diff --git a/tempest/common/generator/base_generator.py b/tempest/common/generator/base_generator.py
index 7e7a2d6..95d50e2 100644
--- a/tempest/common/generator/base_generator.py
+++ b/tempest/common/generator/base_generator.py
@@ -62,7 +62,7 @@
"admin_client": {"type": "boolean"},
"url": {"type": "string"},
"default_result_code": {"type": "integer"},
- "json-schema": jsonschema._utils.load_schema("draft4"),
+ "json-schema": {},
"resources": {
"type": "array",
"items": {
@@ -105,6 +105,8 @@
self.types_dict[type].append(method)
def validate_schema(self, schema):
+ if "json-schema" in schema:
+ jsonschema.Draft4Validator.check_schema(schema['json-schema'])
jsonschema.validate(schema, self.schema)
def generate(self, schema):
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 88dbe58..934b861 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -15,7 +15,7 @@
# under the License.
import collections
-import hashlib
+import inspect
import json
from lxml import etree
import re
@@ -224,44 +224,80 @@
versions = map(lambda x: x['id'], body)
return resp, versions
- def _log_request(self, method, req_url, headers, body):
- self.LOG.info('Request: ' + method + ' ' + req_url)
- if headers:
- print_headers = headers
- if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
- token = headers['X-Auth-Token']
- if len(token) > 64 and TOKEN_CHARS_RE.match(token):
- print_headers = headers.copy()
- print_headers['X-Auth-Token'] = "<Token omitted>"
- self.LOG.debug('Request Headers: ' + str(print_headers))
- if body:
- str_body = str(body)
- length = len(str_body)
- self.LOG.debug('Request Body: ' + str_body[:2048])
- if length >= 2048:
- self.LOG.debug("Large body (%d) md5 summary: %s", length,
- hashlib.md5(str_body).hexdigest())
+ def _find_caller(self):
+ """Find the caller class and test name.
- def _log_response(self, resp, resp_body):
- status = resp['status']
- self.LOG.info("Response Status: " + status)
- headers = resp.copy()
- del headers['status']
- if headers.get('x-compute-request-id'):
- self.LOG.info("Nova/Cinder request id: %s" %
- headers.pop('x-compute-request-id'))
- elif headers.get('x-openstack-request-id'):
- self.LOG.info("OpenStack request id %s" %
- headers.pop('x-openstack-request-id'))
- if len(headers):
- self.LOG.debug('Response Headers: ' + str(headers))
- if resp_body:
- str_body = str(resp_body)
- length = len(str_body)
- self.LOG.debug('Response Body: ' + str_body[:2048])
- if length >= 2048:
- self.LOG.debug("Large body (%d) md5 summary: %s", length,
- hashlib.md5(str_body).hexdigest())
+ Because we know that the interesting things that call us are
+ test_* methods, and various kinds of setUp / tearDown, we
+ can look through the call stack to find appropriate methods,
+ and the class we were in when those were called.
+ """
+ caller_name = None
+ names = []
+ frame = inspect.currentframe()
+ is_cleanup = False
+ # Start climbing the ladder until we hit a good method
+ while True:
+ try:
+ frame = frame.f_back
+ name = frame.f_code.co_name
+ names.append(name)
+ if re.search("^(test_|setUp|tearDown)", name):
+ cname = ""
+ if 'self' in frame.f_locals:
+ cname = frame.f_locals['self'].__class__.__name__
+ if 'cls' in frame.f_locals:
+ cname = frame.f_locals['cls'].__name__
+ caller_name = cname + ":" + name
+ break
+ elif re.search("^_run_cleanup", name):
+ is_cleanup = True
+ else:
+ cname = ""
+ if 'self' in frame.f_locals:
+ cname = frame.f_locals['self'].__class__.__name__
+ if 'cls' in frame.f_locals:
+ cname = frame.f_locals['cls'].__name__
+
+ # the fact that we are running cleanups is indicated pretty
+ # deep in the stack, so if we see that we want to just
+ # start looking for a real class name, and declare victory
+ # once we do.
+ if is_cleanup and cname:
+ if not re.search("^RunTest", cname):
+ caller_name = cname + ":_run_cleanups"
+ break
+ except Exception:
+ break
+ # prevents frame leaks
+ del frame
+ if caller_name is None:
+ self.LOG.debug("Sane call name not found in %s" % names)
+ return caller_name
+
+ def _get_request_id(self, resp):
+ for i in ('x-openstack-request-id', 'x-compute-request-id'):
+ if i in resp:
+ return resp[i]
+ return ""
+
+ def _log_request(self, method, req_url, resp, secs=""):
+ # if we have the request id, put it in the right part of the log
+ extra = dict(request_id=self._get_request_id(resp))
+ # NOTE(sdague): while we still have 6 callers to this function
+ # we're going to just provide work around on who is actually
+ # providing timings by gracefully adding no content if they don't.
+ # Once we're down to 1 caller, clean this up.
+ if secs:
+ secs = " %.3fs" % secs
+ self.LOG.info(
+ 'Request (%s): %s %s %s%s' % (
+ self._find_caller(),
+ resp['status'],
+ method,
+ req_url,
+ secs),
+ extra=extra)
def _parse_resp(self, body):
if self._get_type() is "json":
@@ -340,11 +376,13 @@
# Authenticate the request with the auth provider
req_url, req_headers, req_body = self.auth_provider.auth_request(
method, url, headers, body, self.filters)
- self._log_request(method, req_url, req_headers, req_body)
- # Do the actual request
+
+ # Do the actual request, and time it
+ start = time.time()
resp, resp_body = self.http_obj.request(
req_url, method, headers=req_headers, body=req_body)
- self._log_response(resp, resp_body)
+ end = time.time()
+ self._log_request(method, req_url, resp, secs=(end - start))
# Verify HTTP response codes
self.response_checker(method, url, req_headers, req_body, resp,
resp_body)
diff --git a/tempest/exceptions/__init__.py b/tempest/exceptions/__init__.py
index 485f532..d313def 100644
--- a/tempest/exceptions/__init__.py
+++ b/tempest/exceptions/__init__.py
@@ -158,3 +158,7 @@
class UnexpectedResponseCode(base.RestClientException):
message = "Unexpected response code received"
+
+
+class InvalidStructure(base.TempestException):
+ message = "Invalid structure of table with details"
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index 700a29b..ccb85c4 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute import aggregates as schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -32,6 +33,7 @@
"""Get aggregate list."""
resp, body = self.get("os-aggregates")
body = json.loads(body)
+ self.validate_response(schema.list_aggregates, resp, body)
return resp, body['aggregates']
def get_aggregate(self, aggregate_id):
diff --git a/tempest/services/compute/json/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index a8111af..bc64117 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -16,6 +16,7 @@
import json
import urllib
+from tempest.api_schema.compute import flavors_access as schema_access
from tempest.common import rest_client
from tempest import config
@@ -125,6 +126,7 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id)
body = json.loads(body)
+ self.validate_response(schema_access.list_flavor_access, resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
diff --git a/tempest/services/compute/json/floating_ips_client.py b/tempest/services/compute/json/floating_ips_client.py
index 2a7e25a..273ada6 100644
--- a/tempest/services/compute/json/floating_ips_client.py
+++ b/tempest/services/compute/json/floating_ips_client.py
@@ -47,6 +47,7 @@
body = json.loads(body)
if resp.status == 404:
raise exceptions.NotFound(body)
+ self.validate_response(schema.floating_ip, resp, body)
return resp, body['floating_ip']
def create_floating_ip(self, pool_name=None):
@@ -56,6 +57,7 @@
post_body = json.dumps(post_body)
resp, body = self.post(url, post_body)
body = json.loads(body)
+ self.validate_response(schema.floating_ip, resp, body)
return resp, body['floating_ip']
def delete_floating_ip(self, floating_ip_id):
diff --git a/tempest/services/compute/json/images_client.py b/tempest/services/compute/json/images_client.py
index deb9c93..2f128f2 100644
--- a/tempest/services/compute/json/images_client.py
+++ b/tempest/services/compute/json/images_client.py
@@ -58,6 +58,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_images, resp, body)
return resp, body['images']
def list_images_with_detail(self, params=None):
diff --git a/tempest/services/compute/json/security_groups_client.py b/tempest/services/compute/json/security_groups_client.py
index 899d4ef..9267be7 100644
--- a/tempest/services/compute/json/security_groups_client.py
+++ b/tempest/services/compute/json/security_groups_client.py
@@ -16,6 +16,7 @@
import json
import urllib
+from tempest.api_schema.compute.v2 import security_groups as schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -38,6 +39,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_security_groups, resp, body)
return resp, body['security_groups']
def get_security_group(self, security_group_id):
@@ -119,6 +121,7 @@
"""List all rules for a security group."""
resp, body = self.get('os-security-groups')
body = json.loads(body)
+ self.validate_response(schema.list_security_groups, resp, body)
for sg in body['security_groups']:
if sg['id'] == security_group_id:
return resp, sg['rules']
diff --git a/tempest/services/compute/json/volumes_extensions_client.py b/tempest/services/compute/json/volumes_extensions_client.py
index 451dbac..17468eb 100644
--- a/tempest/services/compute/json/volumes_extensions_client.py
+++ b/tempest/services/compute/json/volumes_extensions_client.py
@@ -42,6 +42,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_volumes, resp, body)
return resp, body['volumes']
def list_volumes_with_detail(self, params=None):
@@ -52,6 +53,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_volumes, resp, body)
return resp, body['volumes']
def get_volume(self, volume_id):
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
index fddf5df..7f73622 100644
--- a/tempest/services/compute/v3/json/aggregates_client.py
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute import aggregates as schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -32,6 +33,7 @@
"""Get aggregate list."""
resp, body = self.get("os-aggregates")
body = json.loads(body)
+ self.validate_response(schema.list_aggregates, resp, body)
return resp, body['aggregates']
def get_aggregate(self, aggregate_id):
diff --git a/tempest/services/compute/v3/json/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
index 656bd84..655e279 100644
--- a/tempest/services/compute/v3/json/flavors_client.py
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -16,6 +16,7 @@
import json
import urllib
+from tempest.api_schema.compute import flavors_access as schema_access
from tempest.common import rest_client
from tempest import config
@@ -125,6 +126,7 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/flavor-access' % flavor_id)
body = json.loads(body)
+ self.validate_response(schema_access.list_flavor_access, resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index 99b4036..58451fb 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -276,10 +276,9 @@
# 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)
- self._log_response(resp, resp_body)
+ self._log_request(method, url, resp)
if resp.status in [401, 403]:
resp_body = json.loads(resp_body)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
index 285feb3..35d8aa0 100644
--- a/tempest/services/identity/v3/json/identity_client.py
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -509,10 +509,9 @@
# 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)
- self._log_response(resp, resp_body)
+ self._log_request(method, url, resp)
if resp.status in [401, 403]:
resp_body = json.loads(resp_body)
diff --git a/tempest/services/identity/v3/xml/identity_client.py b/tempest/services/identity/v3/xml/identity_client.py
index d6c5bc1..8f42924 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -505,10 +505,9 @@
# 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)
- self._log_response(resp, resp_body)
+ self._log_request(method, url, resp)
if resp.status in [401, 403]:
resp_body = json.loads(resp_body)
diff --git a/tempest/services/network/json/network_client.py b/tempest/services/network/json/network_client.py
index a804e8e..27f4655 100644
--- a/tempest/services/network/json/network_client.py
+++ b/tempest/services/network/json/network_client.py
@@ -144,25 +144,6 @@
body = json.loads(body)
return resp, body
- def create_floating_ip(self, ext_network_id, **kwargs):
- post_body = {
- 'floatingip': kwargs}
- post_body['floatingip']['floating_network_id'] = ext_network_id
- body = json.dumps(post_body)
- uri = '%s/floatingips' % (self.uri_prefix)
- resp, body = self.post(uri, body=body)
- body = json.loads(body)
- return resp, body
-
- def update_floating_ip(self, floating_ip_id, **kwargs):
- post_body = {
- 'floatingip': kwargs}
- body = json.dumps(post_body)
- uri = '%s/floatingips/%s' % (self.uri_prefix, floating_ip_id)
- resp, body = self.put(uri, body)
- body = json.loads(body)
- return resp, body
-
def associate_health_monitor_with_pool(self, health_monitor_id,
pool_id):
post_body = {
diff --git a/tempest/services/network/network_client_base.py b/tempest/services/network/network_client_base.py
index 41a7aa4..e21abe1 100644
--- a/tempest/services/network/network_client_base.py
+++ b/tempest/services/network/network_client_base.py
@@ -44,7 +44,6 @@
'security_groups': 'security_groups',
'security_group_rules': 'security_group_rules',
'ikepolicy': 'ikepolicies',
- 'floating_ip': 'floatingips',
'quotas': 'quotas'
}
diff --git a/tempest/services/network/xml/network_client.py b/tempest/services/network/xml/network_client.py
index 2a5083c..68bc424 100644
--- a/tempest/services/network/xml/network_client.py
+++ b/tempest/services/network/xml/network_client.py
@@ -166,33 +166,6 @@
body = _root_tag_fetcher_and_xml_to_json_parse(body)
return resp, body
- def create_floating_ip(self, ext_network_id, **kwargs):
- uri = '%s/floatingips' % (self.uri_prefix)
- floatingip = common.Element('floatingip')
- floatingip.append(common.Element("floating_network_id",
- ext_network_id))
- for element, content in kwargs.iteritems():
- floatingip.append(common.Element(element, content))
- resp, body = self.post(uri, str(common.Document(floatingip)))
- body = _root_tag_fetcher_and_xml_to_json_parse(body)
- return resp, body
-
- def update_floating_ip(self, floating_ip_id, **kwargs):
- uri = '%s/floatingips/%s' % (self.uri_prefix, floating_ip_id)
- floatingip = common.Element('floatingip')
- floatingip.add_attr('xmlns:xsi',
- 'http://www.w3.org/2001/XMLSchema-instance')
- for element, content in kwargs.iteritems():
- if content is None:
- xml_elem = common.Element(element)
- xml_elem.add_attr("xsi:nil", "true")
- floatingip.append(xml_elem)
- else:
- floatingip.append(common.Element(element, content))
- resp, body = self.put(uri, str(common.Document(floatingip)))
- body = _root_tag_fetcher_and_xml_to_json_parse(body)
- return resp, body
-
def list_router_interfaces(self, uuid):
uri = '%s/ports?device_id=%s' % (self.uri_prefix, uuid)
resp, body = self.get(uri)
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
index 7c3fa85..6e7910e 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/services/object_storage/account_client.py
@@ -173,12 +173,11 @@
method=method, url=url, headers=headers, body=body,
filters=self.filters
)
- self._log_request(method, req_url, headers, body)
# use original body
resp, resp_body = self.http_obj.request(req_url, method,
headers=req_headers,
body=req_body)
- self._log_response(resp, resp_body)
+ self._log_request(method, req_url, resp)
if resp.status == 401 or resp.status == 403:
raise exceptions.Unauthorized()
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 77d29a5..49f7f49 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -160,11 +160,10 @@
filters=self.filters
)
# Use original method
- self._log_request(method, req_url, headers, body)
resp, resp_body = self.http_obj.request(req_url, method,
headers=req_headers,
body=req_body)
- self._log_response(resp, resp_body)
+ self._log_request(method, req_url, resp)
if resp.status == 401 or resp.status == 403:
raise exceptions.Unauthorized()
diff --git a/tempest/test.py b/tempest/test.py
index d2568a6..abf42c0 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -24,10 +24,9 @@
import fixtures
import testresources
+import testscenarios
import testtools
-from oslo.config import cfg
-
from tempest import clients
import tempest.common.generator.valid_generator as valid
from tempest.common import isolated_creds
@@ -407,6 +406,24 @@
return json.load(open(fn))
@staticmethod
+ def load_tests(*args):
+ """
+ Wrapper for testscenarios to set the mandatory scenarios variable
+ only in case a real test loader is in place. Will be automatically
+ called in case the variable "load_tests" is set.
+ """
+ if getattr(args[0], 'suiteClass', None) is not None:
+ loader, standard_tests, pattern = args
+ else:
+ standard_tests, module, loader = args
+ for test in testtools.iterate_tests(standard_tests):
+ schema_file = getattr(test, '_schema_file', None)
+ if schema_file is not None:
+ setattr(test, 'scenarios',
+ NegativeAutoTest.generate_scenario(schema_file))
+ return testscenarios.load_tests_apply_scenarios(*args)
+
+ @staticmethod
def generate_scenario(description_file):
"""
Generates the test scenario list for a given description.
@@ -429,17 +446,8 @@
"""
description = NegativeAutoTest.load_schema(description_file)
LOG.debug(description)
-
- # NOTE(mkoderer): since this will be executed on import level the
- # config doesn't have to be in place (e.g. for the pep8 job).
- # In this case simply return.
- try:
- generator = importutils.import_class(
- CONF.negative.test_generator)()
- except cfg.ConfigFilesNotFoundError:
- LOG.critical(
- "Tempest config not found. Test scenarios aren't created")
- return
+ generator = importutils.import_class(
+ CONF.negative.test_generator)()
generator.validate_schema(description)
schema = description.get("json-schema", None)
resources = description.get("resources", [])
diff --git a/tempest/tests/negative/test_negative_generators.py b/tempest/tests/negative/test_negative_generators.py
new file mode 100644
index 0000000..f2ed999
--- /dev/null
+++ b/tempest/tests/negative/test_negative_generators.py
@@ -0,0 +1,81 @@
+# 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 jsonschema
+import mock
+
+import tempest.common.generator.base_generator as base_generator
+from tempest.tests import base
+
+
+class TestNegativeBasicGenerator(base.TestCase):
+ valid_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"]
+ }
+
+ minimal_desc = {
+ "name": "list-flavors-with-detail",
+ "http-method": "GET",
+ "url": "flavors/detail",
+ }
+
+ add_prop_desc = {
+ "name": "list-flavors-with-detail",
+ "http-method": "GET",
+ "url": "flavors/detail",
+ "unknown_field": [12]
+ }
+
+ invalid_json_schema_desc = {
+ "name": "list-flavors-with-detail",
+ "http-method": "GET",
+ "url": "flavors/detail",
+ "json-schema": {"type": "NotExistingType"}
+ }
+
+ def setUp(self):
+ super(TestNegativeBasicGenerator, self).setUp()
+ self.generator = base_generator.BasicGeneratorSet()
+
+ def _assert_valid_jsonschema_call(self, jsonschema_mock, desc):
+ self.assertEqual(jsonschema_mock.call_count, 1)
+ jsonschema_mock.assert_called_with(desc, self.generator.schema)
+
+ @mock.patch('jsonschema.validate', wraps=jsonschema.validate)
+ def test_validate_schema_with_valid_input(self, jsonschema_mock):
+ self.generator.validate_schema(self.valid_desc)
+ self._assert_valid_jsonschema_call(jsonschema_mock, self.valid_desc)
+
+ @mock.patch('jsonschema.validate', wraps=jsonschema.validate)
+ def test_validate_schema_with_minimal_input(self, jsonschema_mock):
+ self.generator.validate_schema(self.minimal_desc)
+ self._assert_valid_jsonschema_call(jsonschema_mock, self.minimal_desc)
+
+ def test_validate_schema_with_invalid_input(self):
+ self.assertRaises(jsonschema.ValidationError,
+ self.generator.validate_schema, self.add_prop_desc)
+ self.assertRaises(jsonschema.SchemaError,
+ self.generator.validate_schema,
+ self.invalid_json_schema_desc)
diff --git a/tempest/tests/test_auth.py b/tempest/tests/test_auth.py
index b6e15bd..62c20e3 100644
--- a/tempest/tests/test_auth.py
+++ b/tempest/tests/test_auth.py
@@ -14,6 +14,7 @@
# under the License.
import copy
+import datetime
from tempest import auth
from tempest.common import http
@@ -131,6 +132,11 @@
self.assertEqual(expected['token'], headers['X-Auth-Token'])
self.assertEqual(expected['body'], body)
+ def _auth_data_with_expiry(self, date_as_string):
+ token, access = self.auth_provider.auth_data
+ access['token']['expires'] = date_as_string
+ return token, access
+
def test_request(self):
filters = {
'service': 'compute',
@@ -292,6 +298,25 @@
expected = 'http://fake_url/'
self._test_base_url_helper(expected, self.filters)
+ def test_token_not_expired(self):
+ expiry_data = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+ auth_data = self._auth_data_with_expiry(
+ expiry_data.strftime(self.auth_provider.EXPIRY_DATE_FORMAT))
+ self.assertFalse(self.auth_provider.is_expired(auth_data))
+
+ def test_token_expired(self):
+ expiry_data = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
+ auth_data = self._auth_data_with_expiry(
+ expiry_data.strftime(self.auth_provider.EXPIRY_DATE_FORMAT))
+ self.assertTrue(self.auth_provider.is_expired(auth_data))
+
+ def test_token_not_expired_to_be_renewed(self):
+ expiry_data = datetime.datetime.utcnow() + \
+ self.auth_provider.token_expiry_threshold / 2
+ auth_data = self._auth_data_with_expiry(
+ expiry_data.strftime(self.auth_provider.EXPIRY_DATE_FORMAT))
+ self.assertTrue(self.auth_provider.is_expired(auth_data))
+
class TestKeystoneV3AuthProvider(TestKeystoneV2AuthProvider):
_endpoints = fake_identity.IDENTITY_V3_RESPONSE['token']['catalog']
@@ -316,6 +341,11 @@
return ep['url'].replace('v3', replacement)
return ep['url']
+ def _auth_data_with_expiry(self, date_as_string):
+ token, access = self.auth_provider.auth_data
+ access['expires_at'] = date_as_string
+ return token, access
+
def test_check_credentials_missing_tenant_name(self):
cred = copy.copy(self.credentials)
del cred['domain_name']
diff --git a/tempest/tests/test_rest_client.py b/tempest/tests/test_rest_client.py
index da9ab72..0677aa0 100644
--- a/tempest/tests/test_rest_client.py
+++ b/tempest/tests/test_rest_client.py
@@ -43,7 +43,7 @@
self.useFixture(mockpatch.PatchObject(self.rest_client, '_get_region',
side_effect=self._get_region()))
self.useFixture(mockpatch.PatchObject(self.rest_client,
- '_log_response'))
+ '_log_request'))
class TestRestClientHTTPMethods(BaseRestClientTestClass):