Merge "Add profiler support into Tempest"
diff --git a/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml b/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml
new file mode 100644
index 0000000..2245044
--- /dev/null
+++ b/releasenotes/notes/add-profiler-config-options-db7c4ae6d338ee5c.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Add support of `OSProfiler library`_ for profiling and distributed
+    tracing of OpenStack. A new config option ``key`` in section ``profiler``
+    is added, the option sets the secret key used to enable profiling in
+    OpenStack services. The value needs to correspond to the one specified
+    in [profiler]/hmac_keys option of OpenStack services.
+
+    .. _OSProfiler library: https://docs.openstack.org/osprofiler/
diff --git a/tempest/config.py b/tempest/config.py
index dc95812..24ae3ae 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -1100,6 +1100,18 @@
 """)
 ]
 
+
+profiler_group = cfg.OptGroup(name="profiler",
+                              title="OpenStack Profiler")
+
+ProfilerGroup = [
+    cfg.StrOpt('key',
+               help="The secret key to enable OpenStack Profiler. The value "
+                    "should match the one configured in OpenStack services "
+                    "under `[profiler]/hmac_keys` property. The default empty "
+                    "value keeps profiling disabled"),
+]
+
 DefaultGroup = [
     cfg.BoolOpt('pause_teardown',
                 default=False,
@@ -1132,6 +1144,7 @@
     (service_available_group, ServiceAvailableGroup),
     (debug_group, DebugGroup),
     (placement_group, PlacementGroup),
+    (profiler_group, ProfilerGroup),
     (None, DefaultGroup)
 ]
 
diff --git a/tempest/lib/common/profiler.py b/tempest/lib/common/profiler.py
new file mode 100644
index 0000000..1544337
--- /dev/null
+++ b/tempest/lib/common/profiler.py
@@ -0,0 +1,64 @@
+#    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 base64
+import hashlib
+import hmac
+import json
+
+from oslo_utils import encodeutils
+from oslo_utils import uuidutils
+
+_profiler = {}
+
+
+def enable(profiler_key, trace_id=None):
+    """Enable global profiler instance
+
+    :param profiler_key: the secret key used to enable profiling in services
+    :param trace_id: unique id of the trace, if empty the id is generated
+        automatically
+    """
+    _profiler['key'] = profiler_key
+    _profiler['uuid'] = trace_id or uuidutils.generate_uuid()
+
+
+def disable():
+    """Disable global profiler instance"""
+    _profiler.clear()
+
+
+def serialize_as_http_headers():
+    """Serialize profiler state as HTTP headers
+
+    This function corresponds to the one from osprofiler library.
+    :return: dictionary with 2 keys `X-Trace-Info` and `X-Trace-HMAC`.
+    """
+    p = _profiler
+    if not p:  # profiler is not enabled
+        return {}
+
+    info = {'base_id': p['uuid'], 'parent_id': p['uuid']}
+    trace_info = base64.urlsafe_b64encode(
+        encodeutils.to_utf8(json.dumps(info)))
+    trace_hmac = _sign(trace_info, p['key'])
+
+    return {
+        'X-Trace-Info': trace_info,
+        'X-Trace-HMAC': trace_hmac,
+    }
+
+
+def _sign(trace_info, key):
+    h = hmac.new(encodeutils.to_utf8(key), digestmod=hashlib.sha1)
+    h.update(trace_info)
+    return h.hexdigest()
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 3be441e..f076727 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -27,6 +27,7 @@
 
 from tempest.lib.common import http
 from tempest.lib.common import jsonschema_validator
+from tempest.lib.common import profiler
 from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions
 
@@ -131,8 +132,10 @@
             accept_type = 'json'
         if send_type is None:
             send_type = 'json'
-        return {'Content-Type': 'application/%s' % send_type,
-                'Accept': 'application/%s' % accept_type}
+        headers = {'Content-Type': 'application/%s' % send_type,
+                   'Accept': 'application/%s' % accept_type}
+        headers.update(profiler.serialize_as_http_headers())
+        return headers
 
     def __str__(self):
         STRING_LIMIT = 80
diff --git a/tempest/test.py b/tempest/test.py
index c3c58dc..85000b6 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -28,6 +28,7 @@
 from tempest.common import utils
 from tempest import config
 from tempest.lib.common import fixed_network
+from tempest.lib.common import profiler
 from tempest.lib.common import validation_resources as vr
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
@@ -231,6 +232,9 @@
         if CONF.pause_teardown:
             BaseTestCase.insert_pdb_breakpoint()
 
+        if CONF.profiler.key:
+            profiler.disable()
+
     @classmethod
     def insert_pdb_breakpoint(cls):
         """Add pdb breakpoint.
@@ -608,6 +612,8 @@
             self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
                                                    format=self.log_format,
                                                    level=None))
+        if CONF.profiler.key:
+            profiler.enable(CONF.profiler.key)
 
     @property
     def credentials_provider(self):
diff --git a/tempest/tests/lib/common/test_profiler.py b/tempest/tests/lib/common/test_profiler.py
new file mode 100644
index 0000000..59fa0364
--- /dev/null
+++ b/tempest/tests/lib/common/test_profiler.py
@@ -0,0 +1,63 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mock
+import testtools
+
+from tempest.lib.common import profiler
+
+
+class TestProfiler(testtools.TestCase):
+
+    def test_serialize(self):
+        key = 'SECRET_KEY'
+        pm = {'key': key, 'uuid': 'ID'}
+
+        with mock.patch('tempest.lib.common.profiler._profiler', pm):
+            with mock.patch('json.dumps') as jdm:
+                jdm.return_value = '{"base_id": "ID", "parent_id": "ID"}'
+
+                expected = {
+                    'X-Trace-HMAC':
+                        '887292df9f13b8b5ecd6bbbd2e16bfaaa4d914b0',
+                    'X-Trace-Info':
+                        b'eyJiYXNlX2lkIjogIklEIiwgInBhcmVudF9pZCI6ICJJRCJ9'
+                }
+
+                self.assertEqual(expected,
+                                 profiler.serialize_as_http_headers())
+
+    def test_profiler_lifecycle(self):
+        key = 'SECRET_KEY'
+        uuid = 'ID'
+
+        self.assertEqual({}, profiler._profiler)
+
+        profiler.enable(key, uuid)
+        self.assertEqual({'key': key, 'uuid': uuid}, profiler._profiler)
+
+        profiler.disable()
+        self.assertEqual({}, profiler._profiler)
+
+    @mock.patch('oslo_utils.uuidutils.generate_uuid')
+    def test_profiler_lifecycle_generate_trace_id(self, generate_uuid_mock):
+        key = 'SECRET_KEY'
+        uuid = 'ID'
+        generate_uuid_mock.return_value = uuid
+
+        self.assertEqual({}, profiler._profiler)
+
+        profiler.enable(key)
+        self.assertEqual({'key': key, 'uuid': uuid}, profiler._profiler)
+
+        profiler.disable()
+        self.assertEqual({}, profiler._profiler)