Merge "Validate get keypair attributes of Nova V2/V3 API"
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/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/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/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/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..41b8fff 100644
--- a/tempest/api_schema/compute/v2/images.py
+++ b/tempest/api_schema/compute/v2/images.py
@@ -79,3 +79,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/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/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/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/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/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/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):