Merge "Remove timer and _log_request() calling"
diff --git a/.zuul.yaml b/.zuul.yaml
index d893483..6685f3f 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -405,6 +405,16 @@
         c-bak: false
 
 - job:
+    name: tempest-full-train
+    parent: tempest-full
+    override-checkout: stable/train
+
+- job:
+    name: tempest-full-train-py3
+    parent: tempest-full-py3
+    override-checkout: stable/train
+
+- job:
     name: tempest-full-stein
     parent: tempest-full
     override-checkout: stable/stein
@@ -622,6 +632,10 @@
         - tempest-full-py3-ipv6:
             voting: false
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-full-train:
+            irrelevant-files: *tempest-irrelevant-files
+        - tempest-full-train-py3:
+            irrelevant-files: *tempest-irrelevant-files
         - tempest-full-stein:
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-stein-py3:
@@ -731,6 +745,8 @@
             irrelevant-files: *tempest-irrelevant-files
     periodic-stable:
       jobs:
+        - tempest-full-train
+        - tempest-full-train-py3
         - tempest-full-stein
         - tempest-full-stein-py3
         - tempest-full-rocky
diff --git a/releasenotes/notes/fix-credential-logging-98089c897d801355.yaml b/releasenotes/notes/fix-credential-logging-98089c897d801355.yaml
new file mode 100644
index 0000000..9534a72
--- /dev/null
+++ b/releasenotes/notes/fix-credential-logging-98089c897d801355.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    A new kwarg, ``log_req_body``, was added to the
+    ``tempest.lib.common.rest_client.RestClient`` method ``raw_request()``.
+    This kwarg takes in a string which will be used in place of the request
+    body, which is logged by default. The intent of this option is to be used
+    for security reasons to avoid logging sensitive information that are part
+    of request bodies.
diff --git a/releasenotes/notes/tempest-train-release-a2ed0743a5eadeb6.yaml b/releasenotes/notes/tempest-train-release-a2ed0743a5eadeb6.yaml
new file mode 100644
index 0000000..960b0b2
--- /dev/null
+++ b/releasenotes/notes/tempest-train-release-a2ed0743a5eadeb6.yaml
@@ -0,0 +1,17 @@
+---
+prelude: |
+    This release is to tag the Tempest for OpenStack Train release.
+    This release marks the start of Train release support in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+    * Train
+    * Stein
+    * Rocky
+    * Queens
+
+    Current development of Tempest is for OpenStack Ussuri development
+    cycle. Every Tempest commit is also tested against master during
+    the Ussuri cycle. However, this does not necessarily mean that using
+    Tempest as of this tag will work against a Ussuri (or future release)
+    cloud.
+    To be on safe side, use this tag to test the OpenStack Train release.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 1d0d914..43c102c 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
    :maxdepth: 1
 
    unreleased
+   v22.0.0
    v21.0.0
    v20.0.0
    v19.0.0
diff --git a/releasenotes/source/v22.0.0.rst b/releasenotes/source/v22.0.0.rst
new file mode 100644
index 0000000..519b081
--- /dev/null
+++ b/releasenotes/source/v22.0.0.rst
@@ -0,0 +1,6 @@
+=====================
+v22.0.0 Release Notes
+=====================
+
+.. release-notes:: 22.0.0 Release Notes
+   :version: 22.0.0
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index c938cee..5a27a43 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -57,6 +57,13 @@
         self.assertIn('status', image)
         self.assertEqual('queued', image['status'])
 
+        # NOTE: This Glance API returns different status codes for image
+        # condition. In this empty data case, Glance should return 204,
+        # so here should check the status code.
+        image_file = self.client.show_image_file(image['id'])
+        self.assertEqual(0, len(image_file.data))
+        self.assertEqual(204, image_file.response.status)
+
         # Now try uploading an image file
         file_content = data_utils.random_bytes()
         image_file = six.BytesIO(file_content)
@@ -115,17 +122,6 @@
                                   visibility='private')
         self.assertEqual('queued', image['status'])
 
-        # NOTE: This Glance API returns different status codes for image
-        # condition. In this empty data case, Glance should return 204,
-        # so here should check the status code.
-        image_file = self.client.show_image_file(image['id'])
-        self.assertEqual(0, len(image_file.data))
-        self.assertEqual(204, image_file.response.status)
-
-        # Now try uploading an image file
-        image_file = six.BytesIO(data_utils.random_bytes())
-        self.client.store_image_file(image['id'], image_file)
-
         # Update Image
         new_image_name = data_utils.rand_name('new-image')
         self.client.update_image(image['id'], [
diff --git a/tempest/api/volume/test_volumes_backup.py b/tempest/api/volume/test_volumes_backup.py
index 6ce5d3e..c178272 100644
--- a/tempest/api/volume/test_volumes_backup.py
+++ b/tempest/api/volume/test_volumes_backup.py
@@ -50,7 +50,6 @@
                                                 'available')
         return restored_volume
 
-    @decorators.skip_because(bug="1483434")
     @testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
                       'ceph does not support arbitrary container names')
     @decorators.idempotent_id('a66eb488-8ee1-47d4-8e9f-575a095728c6')
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 6cc0561..431a0a0 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -556,7 +556,8 @@
 
         return resp, resp_body
 
-    def raw_request(self, url, method, headers=None, body=None, chunked=False):
+    def raw_request(self, url, method, headers=None, body=None, chunked=False,
+                    log_req_body=None):
         """Send a raw HTTP request without the keystone catalog or auth
 
         This method sends a HTTP request in the same manner as the request()
@@ -572,6 +573,11 @@
                              explicitly requires no headers use an empty dict.
         :param str body: Body to send with the request
         :param bool chunked: sends the body with chunked encoding
+        :param str log_req_body: Whether to log the request body or not.
+                                 It is default to None which means request
+                                 body is safe to log otherwise pass any string
+                                 you want to log in place of request body.
+                                 For example: '<omitted>'
         :rtype: tuple
         :return: a tuple with the first entry containing the response headers
                  and the second the response body
@@ -585,8 +591,9 @@
             url, method, headers=headers,
             body=body, chunked=chunked)
         end = time.time()
+        req_body = body if log_req_body is None else log_req_body
         self._log_request(method, url, resp, secs=(end - start),
-                          req_headers=headers, req_body=body,
+                          req_headers=headers, req_body=req_body,
                           resp_body=resp_body)
         return resp, resp_body
 
diff --git a/tempest/lib/services/identity/v2/token_client.py b/tempest/lib/services/identity/v2/token_client.py
index 458c862..9f10f58 100644
--- a/tempest/lib/services/identity/v2/token_client.py
+++ b/tempest/lib/services/identity/v2/token_client.py
@@ -105,9 +105,8 @@
                 headers = self.get_headers(accept_type="json")
 
         resp, resp_body = self.raw_request(url, method,
-                                           headers=headers, body=body)
-        self._log_request(method, url, resp, req_headers=headers,
-                          req_body='<omitted>', resp_body=resp_body)
+                                           headers=headers, body=body,
+                                           log_req_body='<omitted>')
 
         if resp.status in [401, 403]:
             resp_body = json.loads(resp_body)
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index d591f03..6956297 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -160,10 +160,8 @@
                 headers = self.get_headers(accept_type="json")
 
         resp, resp_body = self.raw_request(url, method,
-                                           headers=headers, body=body)
-        self._log_request(method, url, resp, req_headers=headers,
-                          req_body='<omitted>', resp_body=resp_body)
-
+                                           headers=headers, body=body,
+                                           log_req_body='<omitted>')
         if resp.status in [401, 403]:
             resp_body = json.loads(resp_body)
             raise exceptions.Unauthorized(resp_body['error']['message'])
diff --git a/tempest/lib/services/volume/v1/backups_client.py b/tempest/lib/services/volume/v1/backups_client.py
index edc5dda..2289253 100644
--- a/tempest/lib/services/volume/v1/backups_client.py
+++ b/tempest/lib/services/volume/v1/backups_client.py
@@ -83,6 +83,9 @@
 
     def import_backup(self, **kwargs):
         """Import backup metadata record."""
+        # TODO(linanbj): Current api-site doesn't contain this API description.
+        # After fixing the api-site, we need to fix here also for putting the
+        # link to api-site.
         post_body = json.dumps({'backup-record': kwargs})
         resp, body = self.post("backups/import_record", post_body)
         body = json.loads(body)
diff --git a/tempest/test.py b/tempest/test.py
index 438f4d9..1e5cd19 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -203,6 +203,10 @@
             cls._teardowns.append(('resources', cls.resource_cleanup))
             cls.resource_setup()
         except exc as e:
+            # NOTE(dviroel): the exception may be raised after setting up the
+            # user credentials, so we must call tearDownClass to release all
+            # allocated resources.
+            cls.tearDownClass()
             raise exc_to_raise(e.args)
         except Exception:
             etype, value, trace = sys.exc_info()
diff --git a/tempest/tests/lib/fake_identity.py b/tempest/tests/lib/fake_identity.py
index 8bae34f..9d7b0fd 100644
--- a/tempest/tests/lib/fake_identity.py
+++ b/tempest/tests/lib/fake_identity.py
@@ -192,7 +192,7 @@
 
 
 def _fake_v3_response(self, uri, method="GET", body=None, headers=None,
-                      redirections=5, connection_type=None):
+                      redirections=5, connection_type=None, log_req_body=None):
     fake_headers = {
         "x-subject-token": TOKEN
     }
@@ -202,7 +202,7 @@
 
 def _fake_v3_response_domain_scope(self, uri, method="GET", body=None,
                                    headers=None, redirections=5,
-                                   connection_type=None):
+                                   connection_type=None, log_req_body=None):
     fake_headers = {
         "status": "201",
         "x-subject-token": TOKEN
@@ -213,7 +213,7 @@
 
 def _fake_v3_response_no_scope(self, uri, method="GET", body=None,
                                headers=None, redirections=5,
-                               connection_type=None):
+                               connection_type=None, log_req_body=None):
     fake_headers = {
         "status": "201",
         "x-subject-token": TOKEN
@@ -223,7 +223,7 @@
 
 
 def _fake_v2_response(self, uri, method="GET", body=None, headers=None,
-                      redirections=5, connection_type=None):
+                      redirections=5, connection_type=None, log_req_body=None):
     return (fake_http.fake_http_response({}, status=200),
             json.dumps(IDENTITY_V2_RESPONSE))
 
diff --git a/tempest/tests/lib/services/identity/v2/test_token_client.py b/tempest/tests/lib/services/identity/v2/test_token_client.py
index a592ada..5b4e210 100644
--- a/tempest/tests/lib/services/identity/v2/test_token_client.py
+++ b/tempest/tests/lib/services/identity/v2/test_token_client.py
@@ -86,6 +86,9 @@
         with mock.patch.object(token_client_v2, 'raw_request') as mock_raw_r:
             mock_raw_r.return_value = response, body
             resp, body = token_client_v2.request('GET', 'fake_uri')
+        mock_raw_r.assert_called_once_with('fake_uri', 'GET',
+                                           headers=mock.ANY, body=None,
+                                           log_req_body='<omitted>')
         self.assertIsInstance(body, dict)
 
     def test_request_with_bytes_body(self):
diff --git a/tempest/tests/lib/services/identity/v3/test_token_client.py b/tempest/tests/lib/services/identity/v3/test_token_client.py
index a9c58df..656e10a 100644
--- a/tempest/tests/lib/services/identity/v3/test_token_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_token_client.py
@@ -136,6 +136,9 @@
             mock_raw_r.return_value = (
                 fake_identity._fake_v3_response(None, None))
             resp, body = token_client_v3.request('GET', 'fake_uri')
+        mock_raw_r.assert_called_once_with('fake_uri', 'GET',
+                                           headers=mock.ANY, body=None,
+                                           log_req_body='<omitted>')
 
         self.assertIsInstance(body, dict)
 
diff --git a/tempest/tests/lib/services/identity/v3/test_users_client.py b/tempest/tests/lib/services/identity/v3/test_users_client.py
index 5b572f5..c0dfdae 100644
--- a/tempest/tests/lib/services/identity/v3/test_users_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_users_client.py
@@ -25,6 +25,11 @@
             'enabled': True,
             'name': 'Tempest User',
             'password': 'TempestPassword',
+            "description": "Tempest User",
+            "email": "TempestUser@example.com",
+            "options": {
+                "ignore_password_expiry": True
+            }
         }
     }
 
@@ -104,6 +109,38 @@
         ]
     }
 
+    FAKE_PROJECT_LIST = {
+        "links": {
+            "self": "http://example.com/identity/v3/users/313233/projects",
+            "previous": None,
+            "next": None
+        },
+        "projects": [
+            {
+                "description": "description of this project",
+                "domain_id": "161718",
+                "enabled": True,
+                "id": "456788",
+                "links": {
+                    "self": "http://example.com/identity/v3/projects/456788"
+                },
+                "name": "a project name",
+                "parent_id": "212223"
+            },
+            {
+                "description": "description of this project",
+                "domain_id": "161718",
+                "enabled": True,
+                "id": "456789",
+                "links": {
+                    "self": "http://example.com/identity/v3/projects/456789"
+                },
+                "name": "another domain",
+                "parent_id": "212223"
+            },
+        ]
+    }
+
     def setUp(self):
         super(TestUsersClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -155,6 +192,15 @@
             user_id='817fb3c23fd7465ba6d7fe1b1320121d',
         )
 
+    def _test_list_user_projects(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_user_projects,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_PROJECT_LIST,
+            bytes_body,
+            user_id='817fb3c23fd7465ba6d7fe1b1320121d',
+        )
+
     def test_create_user_with_string_body(self):
         self._test_create_user()
 
@@ -185,6 +231,12 @@
     def test_list_user_groups_with_bytes_body(self):
         self._test_list_user_groups(bytes_body=True)
 
+    def test_list_user_projects_with_string_body(self):
+        self._test_list_user_projects()
+
+    def test_list_user_projects_with_bytes_body(self):
+        self._test_list_user_projects(bytes_body=True)
+
     def test_delete_user(self):
         self.check_service_client_function(
             self.client.delete_user,
diff --git a/tempest/tests/test_test.py b/tempest/tests/test_test.py
index ad0793c..a2e0efd 100644
--- a/tempest/tests/test_test.py
+++ b/tempest/tests/test_test.py
@@ -538,7 +538,8 @@
         result = LoggingTestResult(log)
         suite.run(result)
         # If we trigger a skip condition, teardown is not invoked at all
-        self.assertEqual(self.SETUP_FIXTURES[:2],
+        self.assertEqual((self.SETUP_FIXTURES[:2] +
+                          [self.TEARDOWN_FIXTURES[0]]),
                          self.test.fixtures_invoked)
 
     def test_skip_credentials_fails(self):